@proteinjs/user-server 1.22.1 → 1.22.3

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 (152) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +9 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/src/authentication/PasswordHasher.d.ts +9 -0
  9. package/dist/src/authentication/PasswordHasher.d.ts.map +1 -1
  10. package/dist/src/authentication/PasswordHasher.js +25 -0
  11. package/dist/src/authentication/PasswordHasher.js.map +1 -1
  12. package/dist/src/authentication/SocketIOSessionWatcher.d.ts +5 -0
  13. package/dist/src/authentication/SocketIOSessionWatcher.d.ts.map +1 -1
  14. package/dist/src/authentication/SocketIOSessionWatcher.js +13 -2
  15. package/dist/src/authentication/SocketIOSessionWatcher.js.map +1 -1
  16. package/dist/src/authentication/UserStatusTableWatcher.d.ts.map +1 -1
  17. package/dist/src/authentication/UserStatusTableWatcher.js +2 -1
  18. package/dist/src/authentication/UserStatusTableWatcher.js.map +1 -1
  19. package/dist/src/authentication/authenticate.d.ts +15 -0
  20. package/dist/src/authentication/authenticate.d.ts.map +1 -1
  21. package/dist/src/authentication/authenticate.js +55 -20
  22. package/dist/src/authentication/authenticate.js.map +1 -1
  23. package/dist/src/authorization/userCache.d.ts.map +1 -1
  24. package/dist/src/authorization/userCache.js +3 -2
  25. package/dist/src/authorization/userCache.js.map +1 -1
  26. package/dist/src/routes/devLogin.d.ts.map +1 -1
  27. package/dist/src/routes/devLogin.js +29 -14
  28. package/dist/src/routes/devLogin.js.map +1 -1
  29. package/dist/src/routes/executePasswordReset.d.ts +4 -0
  30. package/dist/src/routes/executePasswordReset.d.ts.map +1 -1
  31. package/dist/src/routes/executePasswordReset.js +12 -4
  32. package/dist/src/routes/executePasswordReset.js.map +1 -1
  33. package/dist/src/routes/initiatePasswordReset.d.ts +7 -0
  34. package/dist/src/routes/initiatePasswordReset.d.ts.map +1 -1
  35. package/dist/src/routes/initiatePasswordReset.js +28 -10
  36. package/dist/src/routes/initiatePasswordReset.js.map +1 -1
  37. package/dist/src/routes/login.d.ts +8 -0
  38. package/dist/src/routes/login.d.ts.map +1 -1
  39. package/dist/src/routes/login.js +63 -22
  40. package/dist/src/routes/login.js.map +1 -1
  41. package/dist/src/routes/signup.d.ts.map +1 -1
  42. package/dist/src/routes/signup.js +5 -2
  43. package/dist/src/routes/signup.js.map +1 -1
  44. package/dist/src/routes/validateResetPasswordToken.d.ts.map +1 -1
  45. package/dist/src/routes/validateResetPasswordToken.js +5 -1
  46. package/dist/src/routes/validateResetPasswordToken.js.map +1 -1
  47. package/dist/src/services/MachineCredentials.d.ts +10 -1
  48. package/dist/src/services/MachineCredentials.d.ts.map +1 -1
  49. package/dist/src/services/MachineCredentials.js +46 -15
  50. package/dist/src/services/MachineCredentials.js.map +1 -1
  51. package/dist/src/services/Roles.d.ts.map +1 -1
  52. package/dist/src/services/Roles.js +6 -2
  53. package/dist/src/services/Roles.js.map +1 -1
  54. package/dist/src/services/Signup.d.ts +5 -0
  55. package/dist/src/services/Signup.d.ts.map +1 -1
  56. package/dist/src/services/Signup.js +28 -6
  57. package/dist/src/services/Signup.js.map +1 -1
  58. package/dist/src/throttle/ClientAddress.d.ts +35 -0
  59. package/dist/src/throttle/ClientAddress.d.ts.map +1 -0
  60. package/dist/src/throttle/ClientAddress.js +48 -0
  61. package/dist/src/throttle/ClientAddress.js.map +1 -0
  62. package/dist/src/throttle/PasswordResetThrottle.d.ts +39 -0
  63. package/dist/src/throttle/PasswordResetThrottle.d.ts.map +1 -0
  64. package/dist/src/throttle/PasswordResetThrottle.js +59 -0
  65. package/dist/src/throttle/PasswordResetThrottle.js.map +1 -0
  66. package/dist/src/throttle/RequestDigests.d.ts +42 -0
  67. package/dist/src/throttle/RequestDigests.d.ts.map +1 -0
  68. package/dist/src/throttle/RequestDigests.js +96 -0
  69. package/dist/src/throttle/RequestDigests.js.map +1 -0
  70. package/dist/src/throttle/SignInThrottle.d.ts +67 -0
  71. package/dist/src/throttle/SignInThrottle.d.ts.map +1 -0
  72. package/dist/src/throttle/SignInThrottle.js +91 -0
  73. package/dist/src/throttle/SignInThrottle.js.map +1 -0
  74. package/dist/src/throttle/SlidingWindow.d.ts +51 -0
  75. package/dist/src/throttle/SlidingWindow.d.ts.map +1 -0
  76. package/dist/src/throttle/SlidingWindow.js +85 -0
  77. package/dist/src/throttle/SlidingWindow.js.map +1 -0
  78. package/dist/test/ClientAddress.test.d.ts +2 -0
  79. package/dist/test/ClientAddress.test.d.ts.map +1 -0
  80. package/dist/test/ClientAddress.test.js +51 -0
  81. package/dist/test/ClientAddress.test.js.map +1 -0
  82. package/dist/test/DevLogin.test.js +68 -0
  83. package/dist/test/DevLogin.test.js.map +1 -1
  84. package/dist/test/InitiatePasswordReset.test.js +373 -5
  85. package/dist/test/InitiatePasswordReset.test.js.map +1 -1
  86. package/dist/test/LogCapture.d.ts +10 -0
  87. package/dist/test/LogCapture.d.ts.map +1 -1
  88. package/dist/test/LogCapture.js +19 -0
  89. package/dist/test/LogCapture.js.map +1 -1
  90. package/dist/test/LogCapture.test.d.ts +2 -0
  91. package/dist/test/LogCapture.test.d.ts.map +1 -0
  92. package/dist/test/LogCapture.test.js +86 -0
  93. package/dist/test/LogCapture.test.js.map +1 -0
  94. package/dist/test/LoginThrottle.integration.test.d.ts +2 -0
  95. package/dist/test/LoginThrottle.integration.test.d.ts.map +1 -0
  96. package/dist/test/LoginThrottle.integration.test.js +616 -0
  97. package/dist/test/LoginThrottle.integration.test.js.map +1 -0
  98. package/dist/test/MachineAccounts.integration.test.js +156 -28
  99. package/dist/test/MachineAccounts.integration.test.js.map +1 -1
  100. package/dist/test/NoAddressesInLogs.integration.test.d.ts +2 -0
  101. package/dist/test/NoAddressesInLogs.integration.test.d.ts.map +1 -0
  102. package/dist/test/NoAddressesInLogs.integration.test.js +717 -0
  103. package/dist/test/NoAddressesInLogs.integration.test.js.map +1 -0
  104. package/dist/test/RequestDigests.test.d.ts +2 -0
  105. package/dist/test/RequestDigests.test.d.ts.map +1 -0
  106. package/dist/test/RequestDigests.test.js +73 -0
  107. package/dist/test/RequestDigests.test.js.map +1 -0
  108. package/dist/test/SignupRoute.test.js +80 -1
  109. package/dist/test/SignupRoute.test.js.map +1 -1
  110. package/dist/test/SlidingWindow.test.d.ts +2 -0
  111. package/dist/test/SlidingWindow.test.d.ts.map +1 -0
  112. package/dist/test/SlidingWindow.test.js +72 -0
  113. package/dist/test/SlidingWindow.test.js.map +1 -0
  114. package/dist/test/SocketIOSessionWatcher.test.d.ts +2 -0
  115. package/dist/test/SocketIOSessionWatcher.test.d.ts.map +1 -0
  116. package/dist/test/SocketIOSessionWatcher.test.js +144 -0
  117. package/dist/test/SocketIOSessionWatcher.test.js.map +1 -0
  118. package/generated/index.ts +1 -1
  119. package/index.ts +5 -0
  120. package/package.json +6 -5
  121. package/src/authentication/PasswordHasher.ts +18 -1
  122. package/src/authentication/SocketIOSessionWatcher.ts +13 -1
  123. package/src/authentication/UserStatusTableWatcher.ts +2 -1
  124. package/src/authentication/authenticate.ts +36 -9
  125. package/src/authorization/userCache.ts +3 -2
  126. package/src/routes/devLogin.ts +22 -10
  127. package/src/routes/executePasswordReset.ts +11 -3
  128. package/src/routes/initiatePasswordReset.ts +29 -11
  129. package/src/routes/login.ts +39 -8
  130. package/src/routes/signup.ts +5 -2
  131. package/src/routes/validateResetPasswordToken.ts +5 -1
  132. package/src/services/MachineCredentials.ts +44 -9
  133. package/src/services/Roles.ts +6 -2
  134. package/src/services/Signup.ts +30 -4
  135. package/src/throttle/ClientAddress.ts +47 -0
  136. package/src/throttle/PasswordResetThrottle.ts +62 -0
  137. package/src/throttle/RequestDigests.ts +97 -0
  138. package/src/throttle/SignInThrottle.ts +98 -0
  139. package/src/throttle/SlidingWindow.ts +90 -0
  140. package/test/ClientAddress.test.ts +54 -0
  141. package/test/DevLogin.test.ts +35 -1
  142. package/test/InitiatePasswordReset.test.ts +212 -6
  143. package/test/LogCapture.test.ts +29 -0
  144. package/test/LogCapture.ts +16 -0
  145. package/test/LoginThrottle.integration.test.ts +339 -0
  146. package/test/MachineAccounts.integration.test.ts +97 -22
  147. package/test/NoAddressesInLogs.integration.test.ts +409 -0
  148. package/test/RequestDigests.test.ts +81 -0
  149. package/test/SignupRoute.test.ts +54 -1
  150. package/test/SlidingWindow.test.ts +85 -0
  151. package/test/SocketIOSessionWatcher.test.ts +86 -0
  152. package/test/setup.js +2 -0
@@ -35,7 +35,7 @@ jest.mock('@proteinjs/email-server', () => ({
35
35
 
36
36
  import { getDbAsSystem, Reference } from '@proteinjs/db';
37
37
  import { SourceRepository } from '@proteinjs/reflection';
38
- import { tables } from '@proteinjs/user';
38
+ import { MachineAccount, tables } from '@proteinjs/user';
39
39
  import { signup } from '../src/routes/signup';
40
40
  import { createPassportRequest } from './passportSessionHarness';
41
41
  import { UserServerTestEnvironment } from './UserServerTestEnvironment';
@@ -45,6 +45,31 @@ const testEnv = new UserServerTestEnvironment();
45
45
  const INVITE_CONFIG_KEY = '@proteinjs/user-server/DefaultInviteConfigFactory';
46
46
 
47
47
  type ObjectCache = { objectCache: Record<string, unknown[]> };
48
+ type NamedObjectCache = {
49
+ namedObjectCache: Record<string, { qualifiedName: string; packageName: string; object: unknown }[]>;
50
+ };
51
+
52
+ /** A code-declared machine account owning an address (the declaration only — never booted here). */
53
+ class DeclaredMachineAccount extends MachineAccount {
54
+ id = 'declared-machine';
55
+ email = 'declared.machine@test.local';
56
+ accountName = 'Declared machine';
57
+ roles = [];
58
+ secretName = 'declared-machine-secret';
59
+ }
60
+
61
+ const LOADER_KEY = '@proteinjs/db/SourceRecordLoader';
62
+
63
+ /** Seed the build's source-record declarations (the machine-account read resolves them by name). */
64
+ const setDeclarations = (declarations: unknown[]) => {
65
+ (SourceRepository.get() as unknown as NamedObjectCache).namedObjectCache[LOADER_KEY] = declarations.map(
66
+ (object, i) => ({
67
+ qualifiedName: `@proteinjs/user-server-test/Declared${i}`,
68
+ packageName: '@test/declarer',
69
+ object,
70
+ })
71
+ );
72
+ };
48
73
 
49
74
  /** Seed the invite-config lookup (tests don't load the generated source graph). */
50
75
  const setInviteConfig = (factories: unknown[]) => {
@@ -94,12 +119,14 @@ describe('signup route — auto-login after signup', () => {
94
119
  }, 120000);
95
120
 
96
121
  afterAll(async () => {
122
+ delete (SourceRepository.get() as unknown as NamedObjectCache).namedObjectCache[LOADER_KEY];
97
123
  await testEnv.afterAll();
98
124
  });
99
125
 
100
126
  beforeEach(async () => {
101
127
  jest.clearAllMocks();
102
128
  setInviteConfig([]);
129
+ setDeclarations([]);
103
130
  const db = getDbAsSystem();
104
131
  await db.delete(tables.Invite, {});
105
132
  await db.delete(tables.User, {});
@@ -180,6 +207,32 @@ describe('signup route — auto-login after signup', () => {
180
207
  expect(sendEmail).toHaveBeenCalledTimes(1); // "account already exists" email
181
208
  });
182
209
 
210
+ it(`an address a machine-account declaration owns can't be registered — by signup or by invite: plain words, no account, no session`, async () => {
211
+ setDeclarations([new DeclaredMachineAccount()]);
212
+
213
+ const direct = await invokeSignup({
214
+ name: 'Squatter',
215
+ email: 'Declared.Machine@test.local',
216
+ password: 'pw-squat-1',
217
+ });
218
+ expect(direct.body).toEqual({ error: "This address can't be registered." });
219
+ expect(direct.loggedInAs).toBeUndefined();
220
+ expect(direct.sessionEvents).toEqual([]);
221
+
222
+ await getDbAsSystem().insert(tables.Invite, {
223
+ email: 'declared.machine@test.local',
224
+ token: 'invite-token-declared',
225
+ tokenExpiresAt: moment().add(1, 'day'),
226
+ invitedBy: new Reference(tables.User.name, 'inviter-1'),
227
+ });
228
+ const invited = await invokeSignup({ name: 'Squatter', password: 'pw-squat-2', token: 'invite-token-declared' });
229
+ expect(invited.body).toEqual({ error: "This address can't be registered." });
230
+ expect(invited.loggedInAs).toBeUndefined();
231
+
232
+ expect(await getUserRow('declared.machine@test.local')).toBeUndefined();
233
+ expect(sendEmail).not.toHaveBeenCalled();
234
+ });
235
+
183
236
  it('an unknown invite token gets the honest error — no session, no account', async () => {
184
237
  const outcome = await invokeSignup({
185
238
  name: 'Token Guess',
@@ -0,0 +1,85 @@
1
+ import { SlidingWindow } from '../src/throttle/SlidingWindow';
2
+
3
+ /**
4
+ * The per-key sliding window every throttled door shares (the sign-in and reset doors here, the
5
+ * invite-request door in the consuming app): `hit` records and answers whether the key is over,
6
+ * `isOver` only reads, `record` only counts, `clear` forgets. Attempts refused as over the window
7
+ * are not counted, so a key drains exactly one window after its last counted attempt.
8
+ */
9
+ describe('SlidingWindow', () => {
10
+ const MINUTE = 60 * 1000;
11
+ let now = 1_700_000_000_000;
12
+ const clock = () => now;
13
+
14
+ it('admits up to the limit inside the window and refuses the next', () => {
15
+ const window = new SlidingWindow({ windowMs: 10 * MINUTE, limit: 3, now: clock });
16
+
17
+ expect([window.hit('a'), window.hit('a'), window.hit('a')]).toEqual([false, false, false]);
18
+ expect(window.hit('a')).toBe(true);
19
+ expect(window.isOver('a')).toBe(true);
20
+ });
21
+
22
+ it('keeps each key in its own window', () => {
23
+ const window = new SlidingWindow({ windowMs: 10 * MINUTE, limit: 2, now: clock });
24
+ window.hit('a');
25
+ window.hit('a');
26
+
27
+ expect(window.hit('a')).toBe(true);
28
+ expect(window.hit('b')).toBe(false);
29
+ });
30
+
31
+ it('drains: an attempt older than the window no longer counts, and refused attempts never counted', () => {
32
+ const window = new SlidingWindow({ windowMs: 10 * MINUTE, limit: 2, now: clock });
33
+ window.hit('a');
34
+ now += 4 * MINUTE;
35
+ window.hit('a');
36
+ now += 5 * MINUTE;
37
+ expect(window.hit('a')).toBe(true); // both counted attempts are still inside the window
38
+
39
+ now += 1 * MINUTE + 1; // the first counted attempt left the window; the refused one never counted
40
+ expect(window.hit('a')).toBe(false);
41
+ expect(window.hit('a')).toBe(true);
42
+ });
43
+
44
+ it('isOver reads without counting; record counts without answering; clear forgets the key', () => {
45
+ const window = new SlidingWindow({ windowMs: 10 * MINUTE, limit: 2, now: clock });
46
+
47
+ expect(window.isOver('a')).toBe(false);
48
+ expect(window.isOver('a')).toBe(false);
49
+ window.record('a');
50
+ expect(window.isOver('a')).toBe(false);
51
+ window.record('a');
52
+ expect(window.isOver('a')).toBe(true);
53
+
54
+ window.clear('a');
55
+ expect(window.isOver('a')).toBe(false);
56
+ });
57
+
58
+ it('forgive uncounts the latest attempt only', () => {
59
+ const window = new SlidingWindow({ windowMs: 10 * MINUTE, limit: 2, now: clock });
60
+ window.hit('a');
61
+ window.hit('a');
62
+ expect(window.isOver('a')).toBe(true);
63
+
64
+ window.forgive('a');
65
+
66
+ expect(window.isOver('a')).toBe(false);
67
+ expect(window.hit('a')).toBe(false);
68
+ expect(window.hit('a')).toBe(true);
69
+ window.forgive('b'); // a key never seen: nothing to uncount, nothing thrown
70
+ expect(window.isOver('b')).toBe(false);
71
+ });
72
+
73
+ it('bounds the keys it tracks: at the bound the oldest-touched key is forgotten first', () => {
74
+ const window = new SlidingWindow({ windowMs: 10 * MINUTE, limit: 1, maxKeys: 2, now: clock });
75
+ window.hit('a');
76
+ window.hit('b');
77
+ window.hit('a'); // refused, but 'a' is now the most recently touched
78
+
79
+ window.hit('c'); // at the bound: 'b' (oldest-touched) is forgotten
80
+
81
+ expect(window.isOver('a')).toBe(true);
82
+ expect(window.isOver('b')).toBe(false);
83
+ expect(window.isOver('c')).toBe(true);
84
+ });
85
+ });
@@ -0,0 +1,86 @@
1
+ import { createServer } from 'http';
2
+ import { QueryBuilder } from '@proteinjs/db';
3
+ import { SourceRepository } from '@proteinjs/reflection';
4
+ import { SocketIOServerRepo } from '@proteinjs/server';
5
+ import { Session } from '@proteinjs/user';
6
+ import { SocketIOSessionWatcher } from '../src/authentication/SocketIOSessionWatcher';
7
+
8
+ type ObjectCache = { objectCache: Record<string, unknown[]> };
9
+
10
+ /** What the watcher asked the socket server to do: which rooms (session ids) it closed. */
11
+ type DisconnectCall = { rooms: string[]; close: boolean };
12
+
13
+ /**
14
+ * The socket server this file installs through the repo's own factory seam — a stand-in that
15
+ * records every `in(rooms).disconnectSockets(close)` it receives (no socket.io client is part of
16
+ * this package, so no real socket can connect to observe).
17
+ */
18
+ class RecordingSocketIOServer {
19
+ readonly calls: DisconnectCall[] = [];
20
+
21
+ in(rooms: string | string[]) {
22
+ return {
23
+ disconnectSockets: (close: boolean) => {
24
+ this.calls.push({ rooms: ([] as string[]).concat(rooms), close });
25
+ },
26
+ };
27
+ }
28
+ }
29
+
30
+ const session = (sessionId: string): Session =>
31
+ ({ id: `row-${sessionId}`, sessionId, session: '{}', userEmail: 'someone@test.local' }) as unknown as Session;
32
+
33
+ const deleteSessions = async (sessions: Session[]) =>
34
+ await new SocketIOSessionWatcher().afterDelete(
35
+ sessions.length,
36
+ sessions,
37
+ new QueryBuilder<Session>('session'),
38
+ new QueryBuilder<Session>('session')
39
+ );
40
+
41
+ /**
42
+ * Deleting session rows closes their sockets — and a deletion is not always made by a running
43
+ * server: a boot's source-record sync (and a migration Job, which never starts one) deactivates a
44
+ * withdrawn machine account and deletes its sessions before any socket server exists.
45
+ */
46
+ describe('SocketIOSessionWatcher', () => {
47
+ it('a deletion before the socket server exists resolves — no socket can be open yet', async () => {
48
+ expect(SocketIOServerRepo.getSocketIOServerIfExists()).toBeUndefined();
49
+
50
+ await expect(deleteSessions([session('before-boot')])).resolves.toBeUndefined();
51
+ });
52
+
53
+ describe('with the socket server up', () => {
54
+ const server = new RecordingSocketIOServer();
55
+
56
+ beforeAll(async () => {
57
+ (SourceRepository.get() as unknown as ObjectCache).objectCache['@proteinjs/event/DefaultSocketIOServerFactory'] =
58
+ [{ createSocketIOServer: async () => server }];
59
+ await SocketIOServerRepo.createSocketIOServer(createServer());
60
+ });
61
+
62
+ afterAll(() => {
63
+ delete (SourceRepository.get() as unknown as ObjectCache).objectCache[
64
+ '@proteinjs/event/DefaultSocketIOServerFactory'
65
+ ];
66
+ });
67
+
68
+ beforeEach(() => {
69
+ server.calls.length = 0;
70
+ });
71
+
72
+ it(`closes exactly the deleted sessions' sockets`, async () => {
73
+ await deleteSessions([session('s-1'), session('s-2')]);
74
+
75
+ expect(server.calls).toEqual([{ rooms: ['s-1', 's-2'], close: true }]);
76
+ });
77
+
78
+ it('a deletion with no session ids closes nothing — an empty room list would address every socket', async () => {
79
+ // socket.io's in-memory adapter applies an operation with NO rooms to every socket on the
80
+ // namespace (`apply`: `if (rooms.size) … else for every sid`), so `in([])` must never be sent.
81
+ await deleteSessions([]);
82
+
83
+ expect(server.calls).toEqual([]);
84
+ });
85
+ });
86
+ });
package/test/setup.js CHANGED
@@ -1,2 +1,4 @@
1
1
  process.env.SPANNER_EMULATOR_HOST = process.env.SPANNER_EMULATOR_HOST || 'localhost:9010';
2
2
  process.env.DB_LOG_LEVEL = 'error';
3
+ // The key the throttled doors' digests (RequestDigests) derive from; a process without one refuses to digest.
4
+ process.env.SESSION_SECRET = process.env.SESSION_SECRET || 'user-server-test-secret';