@proteinjs/user-server 1.7.0 → 1.8.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 (102) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/generated/index.d.ts.map +1 -1
  3. package/dist/generated/index.js +9 -1
  4. package/dist/generated/index.js.map +1 -1
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +6 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/src/authentication/PasswordHasher.d.ts +45 -0
  10. package/dist/src/authentication/PasswordHasher.d.ts.map +1 -0
  11. package/dist/src/authentication/PasswordHasher.js +152 -0
  12. package/dist/src/authentication/PasswordHasher.js.map +1 -0
  13. package/dist/src/authentication/UserStatusTableWatcher.d.ts +19 -0
  14. package/dist/src/authentication/UserStatusTableWatcher.d.ts.map +1 -0
  15. package/dist/src/authentication/UserStatusTableWatcher.js +115 -0
  16. package/dist/src/authentication/UserStatusTableWatcher.js.map +1 -0
  17. package/dist/src/authentication/authenticate.d.ts.map +1 -1
  18. package/dist/src/authentication/authenticate.js +41 -13
  19. package/dist/src/authentication/authenticate.js.map +1 -1
  20. package/dist/src/authorization/userCache.d.ts.map +1 -1
  21. package/dist/src/authorization/userCache.js +10 -1
  22. package/dist/src/authorization/userCache.js.map +1 -1
  23. package/dist/src/emails/AccountDeletionEmailConfigs.d.ts +35 -0
  24. package/dist/src/emails/AccountDeletionEmailConfigs.d.ts.map +1 -0
  25. package/dist/src/emails/AccountDeletionEmailConfigs.js +36 -0
  26. package/dist/src/emails/AccountDeletionEmailConfigs.js.map +1 -0
  27. package/dist/src/emails/AccountDeletionEmails.d.ts +13 -0
  28. package/dist/src/emails/AccountDeletionEmails.d.ts.map +1 -0
  29. package/dist/src/emails/AccountDeletionEmails.js +98 -0
  30. package/dist/src/emails/AccountDeletionEmails.js.map +1 -0
  31. package/dist/src/routes/executePasswordReset.js +5 -3
  32. package/dist/src/routes/executePasswordReset.js.map +1 -1
  33. package/dist/src/routes/login.d.ts.map +1 -1
  34. package/dist/src/routes/login.js +26 -2
  35. package/dist/src/routes/login.js.map +1 -1
  36. package/dist/src/services/AccountDeletion.d.ts +52 -0
  37. package/dist/src/services/AccountDeletion.d.ts.map +1 -0
  38. package/dist/src/services/AccountDeletion.js +426 -0
  39. package/dist/src/services/AccountDeletion.js.map +1 -0
  40. package/dist/src/services/MachineCredentials.d.ts +21 -0
  41. package/dist/src/services/MachineCredentials.d.ts.map +1 -0
  42. package/dist/src/services/MachineCredentials.js +164 -0
  43. package/dist/src/services/MachineCredentials.js.map +1 -0
  44. package/dist/src/services/Roles.d.ts.map +1 -1
  45. package/dist/src/services/Roles.js +7 -0
  46. package/dist/src/services/Roles.js.map +1 -1
  47. package/dist/src/services/SetUserStatus.d.ts +18 -0
  48. package/dist/src/services/SetUserStatus.d.ts.map +1 -0
  49. package/dist/src/services/SetUserStatus.js +118 -0
  50. package/dist/src/services/SetUserStatus.js.map +1 -0
  51. package/dist/src/services/Signup.d.ts +2 -2
  52. package/dist/src/services/Signup.d.ts.map +1 -1
  53. package/dist/src/services/Signup.js +22 -17
  54. package/dist/src/services/Signup.js.map +1 -1
  55. package/dist/src/services/UpdateUserInfo.d.ts.map +1 -1
  56. package/dist/src/services/UpdateUserInfo.js +20 -17
  57. package/dist/src/services/UpdateUserInfo.js.map +1 -1
  58. package/dist/test/AccountDeletion.integration.test.d.ts +2 -0
  59. package/dist/test/AccountDeletion.integration.test.d.ts.map +1 -0
  60. package/dist/test/AccountDeletion.integration.test.js +710 -0
  61. package/dist/test/AccountDeletion.integration.test.js.map +1 -0
  62. package/dist/test/DevLogin.test.js +12 -11
  63. package/dist/test/DevLogin.test.js.map +1 -1
  64. package/dist/test/MachineAccounts.integration.test.d.ts +2 -0
  65. package/dist/test/MachineAccounts.integration.test.d.ts.map +1 -0
  66. package/dist/test/MachineAccounts.integration.test.js +597 -0
  67. package/dist/test/MachineAccounts.integration.test.js.map +1 -0
  68. package/dist/test/PasswordMigration.integration.test.d.ts +2 -0
  69. package/dist/test/PasswordMigration.integration.test.d.ts.map +1 -0
  70. package/dist/test/PasswordMigration.integration.test.js +332 -0
  71. package/dist/test/PasswordMigration.integration.test.js.map +1 -0
  72. package/dist/test/SetUserStatus.integration.test.d.ts +2 -0
  73. package/dist/test/SetUserStatus.integration.test.d.ts.map +1 -0
  74. package/dist/test/SetUserStatus.integration.test.js +266 -0
  75. package/dist/test/SetUserStatus.integration.test.js.map +1 -0
  76. package/dist/test/UserStatusGate.integration.test.d.ts +2 -0
  77. package/dist/test/UserStatusGate.integration.test.d.ts.map +1 -0
  78. package/dist/test/UserStatusGate.integration.test.js +162 -0
  79. package/dist/test/UserStatusGate.integration.test.js.map +1 -0
  80. package/generated/index.ts +9 -1
  81. package/index.ts +3 -0
  82. package/package.json +8 -6
  83. package/src/authentication/PasswordHasher.ts +87 -0
  84. package/src/authentication/UserStatusTableWatcher.ts +56 -0
  85. package/src/authentication/authenticate.ts +31 -6
  86. package/src/authorization/userCache.ts +9 -1
  87. package/src/emails/AccountDeletionEmailConfigs.ts +79 -0
  88. package/src/emails/AccountDeletionEmails.ts +39 -0
  89. package/src/routes/executePasswordReset.ts +2 -2
  90. package/src/routes/login.ts +23 -0
  91. package/src/services/AccountDeletion.ts +291 -0
  92. package/src/services/MachineCredentials.ts +94 -0
  93. package/src/services/Roles.ts +10 -0
  94. package/src/services/SetUserStatus.ts +56 -0
  95. package/src/services/Signup.ts +4 -4
  96. package/src/services/UpdateUserInfo.ts +4 -5
  97. package/test/AccountDeletion.integration.test.ts +391 -0
  98. package/test/DevLogin.test.ts +5 -4
  99. package/test/MachineAccounts.integration.test.ts +312 -0
  100. package/test/PasswordMigration.integration.test.ts +132 -0
  101. package/test/SetUserStatus.integration.test.ts +120 -0
  102. package/test/UserStatusGate.integration.test.ts +68 -0
@@ -0,0 +1,710 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __generator = (this && this.__generator) || function (thisArg, body) {
12
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13
+ return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14
+ function verb(n) { return function (v) { return step([n, v]); }; }
15
+ function step(op) {
16
+ if (f) throw new TypeError("Generator is already executing.");
17
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
18
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19
+ if (y = 0, t) op = [op[0] & 2, t.value];
20
+ switch (op[0]) {
21
+ case 0: case 1: t = op; break;
22
+ case 4: _.label++; return { value: op[1], done: false };
23
+ case 5: _.label++; y = op[1]; op = [0]; continue;
24
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
25
+ default:
26
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30
+ if (t[2]) _.ops.pop();
31
+ _.trys.pop(); continue;
32
+ }
33
+ op = body.call(thisArg, _);
34
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36
+ }
37
+ };
38
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
39
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
40
+ if (ar || !(i in from)) {
41
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
42
+ ar[i] = from[i];
43
+ }
44
+ }
45
+ return to.concat(ar || Array.prototype.slice.call(from));
46
+ };
47
+ var __importDefault = (this && this.__importDefault) || function (mod) {
48
+ return (mod && mod.__esModule) ? mod : { "default": mod };
49
+ };
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ var sha256_1 = __importDefault(require("crypto-js/sha256"));
52
+ var moment_1 = __importDefault(require("moment"));
53
+ var db_1 = require("@proteinjs/db");
54
+ var reflection_1 = require("@proteinjs/reflection");
55
+ // Deep import (test-only): ServiceAuth is the real RPC door but is not index-exported by
56
+ // @proteinjs/service — the door test below runs the actual gate rather than pinning metadata.
57
+ var ServiceAuth_1 = require("@proteinjs/service/dist/src/ServiceAuth");
58
+ var user_1 = require("@proteinjs/user");
59
+ var AccountDeletion_1 = require("../src/services/AccountDeletion");
60
+ var authenticate_1 = require("../src/authentication/authenticate");
61
+ var login_1 = require("../src/routes/login");
62
+ var UserServerTestEnvironment_1 = require("./UserServerTestEnvironment");
63
+ var testEnv = new UserServerTestEnvironment_1.UserServerTestEnvironment();
64
+ var PASSWORD_A = 'alpha-password';
65
+ var PASSWORD_B = 'beta-password';
66
+ var RESOURCE_TABLE = 'file';
67
+ /**
68
+ * BUILD §9.1 — the deactivation + cancel suite, two users, outcomes only (rows written against
69
+ * the emulator, never calls made). Fixture: A owns r1/r2 (shared outbound to B), B owns r3
70
+ * (shared inbound to A). A's deletion must revoke both directions while B's own grant — and
71
+ * A's owner grants, the purge walker's terminal targets — survive.
72
+ */
73
+ describe('AccountDeletion — deactivation, manifest, resume, cancel-by-login', function () {
74
+ var userA;
75
+ var userB;
76
+ var r1 = 'resource-a-one';
77
+ var r2 = 'resource-a-two';
78
+ var r3 = 'resource-b-three';
79
+ var ownerA1;
80
+ var ownerA2;
81
+ var outbound1;
82
+ var outbound2;
83
+ var inbound;
84
+ var ownerB;
85
+ beforeAll(function () { return __awaiter(void 0, void 0, void 0, function () {
86
+ return __generator(this, function (_a) {
87
+ switch (_a.label) {
88
+ case 0: return [4 /*yield*/, testEnv.beforeAll()];
89
+ case 1:
90
+ _a.sent();
91
+ return [2 /*return*/];
92
+ }
93
+ });
94
+ }); }, 120000);
95
+ afterAll(function () { return __awaiter(void 0, void 0, void 0, function () {
96
+ return __generator(this, function (_a) {
97
+ switch (_a.label) {
98
+ case 0: return [4 /*yield*/, testEnv.afterAll()];
99
+ case 1:
100
+ _a.sent();
101
+ return [2 /*return*/];
102
+ }
103
+ });
104
+ }); });
105
+ beforeEach(function () { return __awaiter(void 0, void 0, void 0, function () {
106
+ var db;
107
+ return __generator(this, function (_a) {
108
+ switch (_a.label) {
109
+ case 0:
110
+ db = (0, db_1.getDbAsSystem)();
111
+ return [4 /*yield*/, db.delete(user_1.tables.AccountDeletion, {})];
112
+ case 1:
113
+ _a.sent();
114
+ return [4 /*yield*/, db.delete(user_1.tables.AccessGrant, {})];
115
+ case 2:
116
+ _a.sent();
117
+ return [4 /*yield*/, db.delete(user_1.tables.Session, {})];
118
+ case 3:
119
+ _a.sent();
120
+ return [4 /*yield*/, db.delete(user_1.tables.UserStatusEvent, {})];
121
+ case 4:
122
+ _a.sent();
123
+ return [4 /*yield*/, db.delete(user_1.tables.User, {})];
124
+ case 5:
125
+ _a.sent();
126
+ return [4 /*yield*/, createAccount('deleter@test.local', PASSWORD_A)];
127
+ case 6:
128
+ userA = _a.sent();
129
+ return [4 /*yield*/, createAccount('keeper@test.local', PASSWORD_B)];
130
+ case 7:
131
+ userB = _a.sent();
132
+ return [4 /*yield*/, insertGrant(userA.id, r1, 'owner')];
133
+ case 8:
134
+ ownerA1 = _a.sent();
135
+ return [4 /*yield*/, insertGrant(userA.id, r2, 'owner')];
136
+ case 9:
137
+ ownerA2 = _a.sent();
138
+ return [4 /*yield*/, insertGrant(userB.id, r1, 'read')];
139
+ case 10:
140
+ outbound1 = _a.sent();
141
+ return [4 /*yield*/, insertGrant(userB.id, r2, 'write')];
142
+ case 11:
143
+ outbound2 = _a.sent();
144
+ return [4 /*yield*/, insertGrant(userB.id, r3, 'owner')];
145
+ case 12:
146
+ ownerB = _a.sent();
147
+ return [4 /*yield*/, insertGrant(userA.id, r3, 'read')];
148
+ case 13:
149
+ inbound = _a.sent();
150
+ return [4 /*yield*/, insertSession('session-a-1', userA.email)];
151
+ case 14:
152
+ _a.sent();
153
+ return [4 /*yield*/, insertSession('session-a-2', userA.email)];
154
+ case 15:
155
+ _a.sent();
156
+ return [4 /*yield*/, insertSession('session-b-1', userB.email)];
157
+ case 16:
158
+ _a.sent();
159
+ return [2 /*return*/];
160
+ }
161
+ });
162
+ }); });
163
+ /** Insert an account the way the product stores it: lowercased email, sha256 password. */
164
+ var createAccount = function (email, password) { return __awaiter(void 0, void 0, void 0, function () {
165
+ return __generator(this, function (_a) {
166
+ switch (_a.label) {
167
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().insert(user_1.tables.User, {
168
+ name: 'Deletion test user',
169
+ email: email,
170
+ password: (0, sha256_1.default)(password).toString(),
171
+ emailVerified: true,
172
+ roles: [],
173
+ })];
174
+ case 1: return [2 /*return*/, _a.sent()];
175
+ }
176
+ });
177
+ }); };
178
+ var insertGrant = function (principalId, resourceId, accessLevel) { return __awaiter(void 0, void 0, void 0, function () {
179
+ return __generator(this, function (_a) {
180
+ switch (_a.label) {
181
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().insert(user_1.tables.AccessGrant, {
182
+ principal: new db_1.Reference(user_1.tables.User.name, principalId),
183
+ resource: new db_1.Reference(RESOURCE_TABLE, resourceId),
184
+ resourceTable: RESOURCE_TABLE,
185
+ accessLevel: accessLevel,
186
+ })];
187
+ case 1: return [2 /*return*/, _a.sent()];
188
+ }
189
+ });
190
+ }); };
191
+ var insertSession = function (sessionId, userEmail) { return __awaiter(void 0, void 0, void 0, function () {
192
+ return __generator(this, function (_a) {
193
+ switch (_a.label) {
194
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().insert(user_1.tables.Session, {
195
+ sessionId: sessionId,
196
+ session: '{}',
197
+ expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
198
+ userEmail: userEmail,
199
+ })];
200
+ case 1: return [2 /*return*/, _a.sent()];
201
+ }
202
+ });
203
+ }); };
204
+ var grantRows = function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
205
+ switch (_a.label) {
206
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(user_1.tables.AccessGrant, {})];
207
+ case 1: return [2 /*return*/, _a.sent()];
208
+ }
209
+ }); }); };
210
+ var sessionRows = function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
211
+ switch (_a.label) {
212
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(user_1.tables.Session, {})];
213
+ case 1: return [2 /*return*/, _a.sent()];
214
+ }
215
+ }); }); };
216
+ var auditRows = function () { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
217
+ switch (_a.label) {
218
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(user_1.tables.UserStatusEvent, {})];
219
+ case 1: return [2 /*return*/, _a.sent()];
220
+ }
221
+ }); }); };
222
+ var deletionRow = function (userId) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
223
+ switch (_a.label) {
224
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(user_1.tables.AccountDeletion, { userId: userId })];
225
+ case 1: return [2 /*return*/, _a.sent()];
226
+ }
227
+ }); }); };
228
+ var userRow = function (id) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
229
+ switch (_a.label) {
230
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().get(user_1.tables.User, { id: id })];
231
+ case 1: return [2 /*return*/, _a.sent()];
232
+ }
233
+ }); }); };
234
+ var sortById = function (rows) { return __spreadArray([], rows, true).sort(function (a, b) { return a.id.localeCompare(b.id); }); };
235
+ var expectedManifest = function () { return [
236
+ { id: inbound.id, principal: userA.id, resource: r3, resourceTable: RESOURCE_TABLE, accessLevel: 'read' },
237
+ { id: outbound1.id, principal: userB.id, resource: r1, resourceTable: RESOURCE_TABLE, accessLevel: 'read' },
238
+ { id: outbound2.id, principal: userB.id, resource: r2, resourceTable: RESOURCE_TABLE, accessLevel: 'write' },
239
+ ]; };
240
+ /** Drive the real login route with a fake request/response pair. */
241
+ var invokeLogin = function (email, password) { return __awaiter(void 0, void 0, void 0, function () {
242
+ var loggedInAs, sent, response, request;
243
+ return __generator(this, function (_a) {
244
+ switch (_a.label) {
245
+ case 0:
246
+ response = {
247
+ send: function (body) {
248
+ sent = body;
249
+ },
250
+ status: function () { return response; },
251
+ };
252
+ request = {
253
+ body: { email: email, password: password },
254
+ login: function (loginEmail, callback) {
255
+ loggedInAs = loginEmail;
256
+ callback();
257
+ },
258
+ };
259
+ return [4 /*yield*/, login_1.login.onRequest(request, response)];
260
+ case 1:
261
+ _a.sent();
262
+ return [2 /*return*/, { loggedInAs: loggedInAs, sent: sent }];
263
+ }
264
+ });
265
+ }); };
266
+ it('the service door admits any signed-in account and refuses guests', function () {
267
+ // ServiceAuth funnels through UserAuth, which resolves the current user from the
268
+ // source-graph-registered AuthenticatedUserRepo — seed it the way the env seeds session
269
+ // storage (tests do not load the generated source graph).
270
+ reflection_1.SourceRepository.get().objectCache['@proteinjs/user-auth/AuthenticatedUserRepo'] = [new user_1.UserRepo()];
271
+ var canRun = function () {
272
+ return ServiceAuth_1.ServiceAuth.canRunService(new AccountDeletion_1.AccountDeletion(), { name: 'requestDeletion' }, [PASSWORD_A]);
273
+ };
274
+ testEnv.actAs(userA); // a plain account with no roles at all
275
+ expect(canRun()).toBe(true);
276
+ testEnv.actAs(user_1.guestUser);
277
+ expect(canRun()).toBe(false);
278
+ });
279
+ it('deactivates: revokes grants both directions, persists the full manifest, flips + audits standing, kills sessions', function () { return __awaiter(void 0, void 0, void 0, function () {
280
+ var result, remaining, deletion, hoursOut, flipped, events, sessions;
281
+ return __generator(this, function (_a) {
282
+ switch (_a.label) {
283
+ case 0:
284
+ testEnv.actAs(userA);
285
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().requestDeletion(PASSWORD_A)];
286
+ case 1:
287
+ result = _a.sent();
288
+ return [4 /*yield*/, grantRows()];
289
+ case 2:
290
+ remaining = _a.sent();
291
+ expect(sortById(remaining).map(function (grant) { return grant.id; })).toEqual(sortById([ownerA1, ownerA2, ownerB]).map(function (grant) { return grant.id; }));
292
+ return [4 /*yield*/, deletionRow(userA.id)];
293
+ case 3:
294
+ deletion = _a.sent();
295
+ expect(deletion).toBeTruthy();
296
+ expect(deletion.phase).toBe('grace');
297
+ expect(deletion.leaseSeq).toBe(0);
298
+ expect(deletion.userEmail).toBe(userA.email);
299
+ expect(__spreadArray([], deletion.ownedResourceIds, true).sort()).toEqual([r1, r2].sort());
300
+ expect(sortById(deletion.manifestGrants)).toEqual(sortById(expectedManifest()));
301
+ hoursOut = (0, moment_1.default)(deletion.purgeAfter).diff((0, moment_1.default)(), 'hours', true);
302
+ expect(hoursOut).toBeGreaterThan(719);
303
+ expect(hoursOut).toBeLessThan(721);
304
+ expect(Math.abs(result.purgeAfter.valueOf() - (0, moment_1.default)(deletion.purgeAfter).valueOf())).toBeLessThan(1000);
305
+ return [4 /*yield*/, userRow(userA.id)];
306
+ case 4:
307
+ flipped = _a.sent();
308
+ expect(flipped.status).toBe('deactivated');
309
+ expect(flipped.deleteRequestedAt).toBeTruthy();
310
+ expect(Math.abs((0, moment_1.default)(flipped.purgeAfter).valueOf() - (0, moment_1.default)(deletion.purgeAfter).valueOf())).toBeLessThan(1000);
311
+ return [4 /*yield*/, auditRows()];
312
+ case 5:
313
+ events = _a.sent();
314
+ expect(events).toHaveLength(1);
315
+ expect(events[0]).toMatchObject({ actor: userA.id, target: userA.id, status: 'deactivated' });
316
+ return [4 /*yield*/, sessionRows()];
317
+ case 6:
318
+ sessions = _a.sent();
319
+ expect(sessions).toHaveLength(1);
320
+ expect(sessions[0].userEmail).toBe(userB.email);
321
+ return [2 /*return*/];
322
+ }
323
+ });
324
+ }); });
325
+ it('refuses a wrong password with zero writes', function () { return __awaiter(void 0, void 0, void 0, function () {
326
+ var _a, _b, _c, _d, untouched;
327
+ return __generator(this, function (_e) {
328
+ switch (_e.label) {
329
+ case 0:
330
+ testEnv.actAs(userA);
331
+ return [4 /*yield*/, expect(new AccountDeletion_1.AccountDeletion().requestDeletion('wrong-password')).rejects.toThrow('Password incorrect')];
332
+ case 1:
333
+ _e.sent();
334
+ _a = expect;
335
+ return [4 /*yield*/, deletionRow(userA.id)];
336
+ case 2:
337
+ _a.apply(void 0, [_e.sent()]).toBeFalsy();
338
+ _b = expect;
339
+ return [4 /*yield*/, grantRows()];
340
+ case 3:
341
+ _b.apply(void 0, [_e.sent()]).toHaveLength(6);
342
+ _c = expect;
343
+ return [4 /*yield*/, sessionRows()];
344
+ case 4:
345
+ _c.apply(void 0, [_e.sent()]).toHaveLength(3);
346
+ _d = expect;
347
+ return [4 /*yield*/, auditRows()];
348
+ case 5:
349
+ _d.apply(void 0, [_e.sent()]).toHaveLength(0);
350
+ return [4 /*yield*/, userRow(userA.id)];
351
+ case 6:
352
+ untouched = _e.sent();
353
+ expect(untouched.status).toBe('active');
354
+ expect(untouched.deleteRequestedAt).toBeFalsy();
355
+ expect(untouched.purgeAfter).toBeFalsy();
356
+ return [2 /*return*/];
357
+ }
358
+ });
359
+ }); });
360
+ it('resume keeps the FULL manifest after a crash mid-revocation — never re-enumerates', function () { return __awaiter(void 0, void 0, void 0, function () {
361
+ var service, persisted, _i, _a, grant, resumed, remaining, _b, _c;
362
+ return __generator(this, function (_d) {
363
+ switch (_d.label) {
364
+ case 0:
365
+ testEnv.actAs(userA);
366
+ service = new AccountDeletion_1.AccountDeletion();
367
+ return [4 /*yield*/, service.requestDeletion(PASSWORD_A)];
368
+ case 1:
369
+ _d.sent();
370
+ return [4 /*yield*/, deletionRow(userA.id)];
371
+ case 2:
372
+ persisted = _d.sent();
373
+ expect(persisted.manifestGrants).toHaveLength(3);
374
+ _i = 0, _a = [outbound1, outbound2];
375
+ _d.label = 3;
376
+ case 3:
377
+ if (!(_i < _a.length)) return [3 /*break*/, 6];
378
+ grant = _a[_i];
379
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().insert(user_1.tables.AccessGrant, {
380
+ id: grant.id,
381
+ principal: new db_1.Reference(user_1.tables.User.name, grant.principal._id),
382
+ resource: new db_1.Reference(RESOURCE_TABLE, grant.resource._id),
383
+ resourceTable: RESOURCE_TABLE,
384
+ accessLevel: grant.accessLevel,
385
+ })];
386
+ case 4:
387
+ _d.sent();
388
+ _d.label = 5;
389
+ case 5:
390
+ _i++;
391
+ return [3 /*break*/, 3];
392
+ case 6: return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, {
393
+ id: userA.id,
394
+ status: 'active',
395
+ deleteRequestedAt: null,
396
+ purgeAfter: null,
397
+ })];
398
+ case 7:
399
+ _d.sent();
400
+ return [4 /*yield*/, insertSession('session-a-3', userA.email)];
401
+ case 8:
402
+ _d.sent();
403
+ // The user's retry resumes from the stored manifest.
404
+ return [4 /*yield*/, service.requestDeletion(PASSWORD_A)];
405
+ case 9:
406
+ // The user's retry resumes from the stored manifest.
407
+ _d.sent();
408
+ return [4 /*yield*/, deletionRow(userA.id)];
409
+ case 10:
410
+ resumed = _d.sent();
411
+ expect(sortById(resumed.manifestGrants)).toEqual(sortById(expectedManifest()));
412
+ expect(__spreadArray([], resumed.ownedResourceIds, true).sort()).toEqual([r1, r2].sort());
413
+ return [4 /*yield*/, grantRows()];
414
+ case 11:
415
+ remaining = _d.sent();
416
+ expect(sortById(remaining).map(function (grant) { return grant.id; })).toEqual(sortById([ownerA1, ownerA2, ownerB]).map(function (grant) { return grant.id; }));
417
+ _b = expect;
418
+ return [4 /*yield*/, userRow(userA.id)];
419
+ case 12:
420
+ _b.apply(void 0, [(_d.sent()).status]).toBe('deactivated');
421
+ _c = expect;
422
+ return [4 /*yield*/, sessionRows()];
423
+ case 13:
424
+ _c.apply(void 0, [(_d.sent()).map(function (session) { return session.userEmail; })]).toEqual([userB.email]);
425
+ return [2 /*return*/];
426
+ }
427
+ });
428
+ }); });
429
+ it('cancel restores grants (fresh ids), reactivates + audits, nulls stamps, deletes the manifest row', function () { return __awaiter(void 0, void 0, void 0, function () {
430
+ var manifest, outcome, all, _loop_1, _i, manifest_1, entry, restored, events, _a;
431
+ return __generator(this, function (_b) {
432
+ switch (_b.label) {
433
+ case 0:
434
+ testEnv.actAs(userA);
435
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().requestDeletion(PASSWORD_A)];
436
+ case 1:
437
+ _b.sent();
438
+ return [4 /*yield*/, deletionRow(userA.id)];
439
+ case 2:
440
+ manifest = (_b.sent()).manifestGrants;
441
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().cancelPendingDeletion(userA.email)];
442
+ case 3:
443
+ outcome = _b.sent();
444
+ expect(outcome).toBe('restored');
445
+ return [4 /*yield*/, grantRows()];
446
+ case 4:
447
+ all = _b.sent();
448
+ expect(all).toHaveLength(6);
449
+ _loop_1 = function (entry) {
450
+ var matches = all.filter(function (grant) {
451
+ return grant.principal._id === entry.principal &&
452
+ grant.resource._id === entry.resource &&
453
+ grant.accessLevel === entry.accessLevel;
454
+ });
455
+ expect(matches).toHaveLength(1);
456
+ expect(matches[0].id).not.toBe(entry.id);
457
+ };
458
+ for (_i = 0, manifest_1 = manifest; _i < manifest_1.length; _i++) {
459
+ entry = manifest_1[_i];
460
+ _loop_1(entry);
461
+ }
462
+ return [4 /*yield*/, userRow(userA.id)];
463
+ case 5:
464
+ restored = _b.sent();
465
+ expect(restored.status).toBe('active');
466
+ expect(restored.deleteRequestedAt).toBeFalsy();
467
+ expect(restored.purgeAfter).toBeFalsy();
468
+ return [4 /*yield*/, auditRows()];
469
+ case 6:
470
+ events = _b.sent();
471
+ expect(events.map(function (event) { return event.status; }).sort()).toEqual(['active', 'deactivated']);
472
+ _a = expect;
473
+ return [4 /*yield*/, deletionRow(userA.id)];
474
+ case 7:
475
+ _a.apply(void 0, [_b.sent()]).toBeFalsy();
476
+ return [2 /*return*/];
477
+ }
478
+ });
479
+ }); });
480
+ it('cancel resumes a crashed prior cancel: restoring-phase re-entry, no duplicate grants', function () { return __awaiter(void 0, void 0, void 0, function () {
481
+ var deletion, first, outcome, all, _loop_2, _i, _a, entry, _b, _c;
482
+ return __generator(this, function (_d) {
483
+ switch (_d.label) {
484
+ case 0:
485
+ testEnv.actAs(userA);
486
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().requestDeletion(PASSWORD_A)];
487
+ case 1:
488
+ _d.sent();
489
+ return [4 /*yield*/, deletionRow(userA.id)];
490
+ case 2:
491
+ deletion = _d.sent();
492
+ // A prior cancel claimed the row, restored ONE grant, then crashed.
493
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.AccountDeletion, { id: deletion.id, phase: 'restoring' })];
494
+ case 3:
495
+ // A prior cancel claimed the row, restored ONE grant, then crashed.
496
+ _d.sent();
497
+ first = deletion.manifestGrants[0];
498
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().insert(user_1.tables.AccessGrant, {
499
+ principal: new db_1.Reference(user_1.tables.User.name, first.principal),
500
+ resource: new db_1.Reference(first.resourceTable, first.resource),
501
+ resourceTable: first.resourceTable,
502
+ accessLevel: first.accessLevel,
503
+ })];
504
+ case 4:
505
+ _d.sent();
506
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().cancelPendingDeletion(userA.email)];
507
+ case 5:
508
+ outcome = _d.sent();
509
+ expect(outcome).toBe('restored');
510
+ return [4 /*yield*/, grantRows()];
511
+ case 6:
512
+ all = _d.sent();
513
+ expect(all).toHaveLength(6);
514
+ _loop_2 = function (entry) {
515
+ var matches = all.filter(function (grant) {
516
+ return grant.principal._id === entry.principal &&
517
+ grant.resource._id === entry.resource &&
518
+ grant.accessLevel === entry.accessLevel;
519
+ });
520
+ expect(matches).toHaveLength(1);
521
+ };
522
+ for (_i = 0, _a = deletion.manifestGrants; _i < _a.length; _i++) {
523
+ entry = _a[_i];
524
+ _loop_2(entry);
525
+ }
526
+ _b = expect;
527
+ return [4 /*yield*/, userRow(userA.id)];
528
+ case 7:
529
+ _b.apply(void 0, [(_d.sent()).status]).toBe('active');
530
+ _c = expect;
531
+ return [4 /*yield*/, deletionRow(userA.id)];
532
+ case 8:
533
+ _c.apply(void 0, [_d.sent()]).toBeFalsy();
534
+ return [2 /*return*/];
535
+ }
536
+ });
537
+ }); });
538
+ it('cancel refuses once the purge walker has claimed the row', function () { return __awaiter(void 0, void 0, void 0, function () {
539
+ var deletion, outcome, _a, _b, _c;
540
+ return __generator(this, function (_d) {
541
+ switch (_d.label) {
542
+ case 0:
543
+ testEnv.actAs(userA);
544
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().requestDeletion(PASSWORD_A)];
545
+ case 1:
546
+ _d.sent();
547
+ return [4 /*yield*/, deletionRow(userA.id)];
548
+ case 2:
549
+ deletion = _d.sent();
550
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.AccountDeletion, { id: deletion.id, phase: 'purging' })];
551
+ case 3:
552
+ _d.sent();
553
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().cancelPendingDeletion(userA.email)];
554
+ case 4:
555
+ outcome = _d.sent();
556
+ expect(outcome).toBe('purging');
557
+ // Nothing restored: standing stays deactivated, grants stay revoked, manifest row stays.
558
+ _a = expect;
559
+ return [4 /*yield*/, userRow(userA.id)];
560
+ case 5:
561
+ // Nothing restored: standing stays deactivated, grants stay revoked, manifest row stays.
562
+ _a.apply(void 0, [(_d.sent()).status]).toBe('deactivated');
563
+ _b = expect;
564
+ return [4 /*yield*/, grantRows()];
565
+ case 6:
566
+ _b.apply(void 0, [_d.sent()]).toHaveLength(3);
567
+ _c = expect;
568
+ return [4 /*yield*/, deletionRow(userA.id)];
569
+ case 7:
570
+ _c.apply(void 0, [(_d.sent()).phase]).toBe('purging');
571
+ return [2 /*return*/];
572
+ }
573
+ });
574
+ }); });
575
+ it('cancel is a no-op for an account with no pending deletion', function () { return __awaiter(void 0, void 0, void 0, function () {
576
+ var _a, _b;
577
+ return __generator(this, function (_c) {
578
+ switch (_c.label) {
579
+ case 0:
580
+ _a = expect;
581
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().cancelPendingDeletion(userB.email)];
582
+ case 1:
583
+ _a.apply(void 0, [_c.sent()]).toBe('not-pending');
584
+ _b = expect;
585
+ return [4 /*yield*/, grantRows()];
586
+ case 2:
587
+ _b.apply(void 0, [_c.sent()]).toHaveLength(6);
588
+ return [2 /*return*/];
589
+ }
590
+ });
591
+ }); });
592
+ it('authenticate matrix: active, staff-deactivated, pending-deletion, mid-purge', function () { return __awaiter(void 0, void 0, void 0, function () {
593
+ var _a, _b, _c, deletion, _d;
594
+ return __generator(this, function (_e) {
595
+ switch (_e.label) {
596
+ case 0:
597
+ // Active + correct credentials.
598
+ _a = expect;
599
+ return [4 /*yield*/, (0, authenticate_1.authenticate)(userB.email, PASSWORD_B)];
600
+ case 1:
601
+ // Active + correct credentials.
602
+ _a.apply(void 0, [_e.sent()]).toBe(true);
603
+ // Staff deactivation (no deleteRequestedAt) stays refused.
604
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, { id: userB.id, status: 'deactivated' })];
605
+ case 2:
606
+ // Staff deactivation (no deleteRequestedAt) stays refused.
607
+ _e.sent();
608
+ _b = expect;
609
+ return [4 /*yield*/, (0, authenticate_1.authenticate)(userB.email, PASSWORD_B)];
610
+ case 3:
611
+ _b.apply(void 0, [_e.sent()]).toBe('This account has been deactivated');
612
+ // Pending deletion: credentials valid — the login route decides (cancel hook).
613
+ testEnv.actAs(userA);
614
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().requestDeletion(PASSWORD_A)];
615
+ case 4:
616
+ _e.sent();
617
+ _c = expect;
618
+ return [4 /*yield*/, (0, authenticate_1.authenticate)(userA.email, PASSWORD_A)];
619
+ case 5:
620
+ _c.apply(void 0, [_e.sent()]).toBe(true);
621
+ return [4 /*yield*/, deletionRow(userA.id)];
622
+ case 6:
623
+ deletion = _e.sent();
624
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.AccountDeletion, { id: deletion.id, phase: 'purging' })];
625
+ case 7:
626
+ _e.sent();
627
+ _d = expect;
628
+ return [4 /*yield*/, (0, authenticate_1.authenticate)(userA.email, PASSWORD_A)];
629
+ case 8:
630
+ _d.apply(void 0, [_e.sent()]).toBe(true);
631
+ return [2 /*return*/];
632
+ }
633
+ });
634
+ }); });
635
+ it('cancel-by-login: logging back in during grace restores the account before the session starts', function () { return __awaiter(void 0, void 0, void 0, function () {
636
+ var _a, loggedInAs, sent, _b, _c, _d;
637
+ return __generator(this, function (_e) {
638
+ switch (_e.label) {
639
+ case 0:
640
+ testEnv.actAs(userA);
641
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().requestDeletion(PASSWORD_A)];
642
+ case 1:
643
+ _e.sent();
644
+ return [4 /*yield*/, invokeLogin(userA.email, PASSWORD_A)];
645
+ case 2:
646
+ _a = _e.sent(), loggedInAs = _a.loggedInAs, sent = _a.sent;
647
+ expect(sent).toEqual({});
648
+ expect(loggedInAs).toBe(userA.email);
649
+ _b = expect;
650
+ return [4 /*yield*/, userRow(userA.id)];
651
+ case 3:
652
+ _b.apply(void 0, [(_e.sent()).status]).toBe('active');
653
+ _c = expect;
654
+ return [4 /*yield*/, grantRows()];
655
+ case 4:
656
+ _c.apply(void 0, [_e.sent()]).toHaveLength(6);
657
+ _d = expect;
658
+ return [4 /*yield*/, deletionRow(userA.id)];
659
+ case 5:
660
+ _d.apply(void 0, [_e.sent()]).toBeFalsy();
661
+ return [2 /*return*/];
662
+ }
663
+ });
664
+ }); });
665
+ it('login refuses a mid-purge account with the no-longer-restorable message', function () { return __awaiter(void 0, void 0, void 0, function () {
666
+ var deletion, _a, loggedInAs, sent, _b;
667
+ return __generator(this, function (_c) {
668
+ switch (_c.label) {
669
+ case 0:
670
+ testEnv.actAs(userA);
671
+ return [4 /*yield*/, new AccountDeletion_1.AccountDeletion().requestDeletion(PASSWORD_A)];
672
+ case 1:
673
+ _c.sent();
674
+ return [4 /*yield*/, deletionRow(userA.id)];
675
+ case 2:
676
+ deletion = _c.sent();
677
+ return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.AccountDeletion, { id: deletion.id, phase: 'purging' })];
678
+ case 3:
679
+ _c.sent();
680
+ return [4 /*yield*/, invokeLogin(userA.email, PASSWORD_A)];
681
+ case 4:
682
+ _a = _c.sent(), loggedInAs = _a.loggedInAs, sent = _a.sent;
683
+ expect(sent).toEqual({ error: 'This account is being deleted and can no longer be restored.' });
684
+ expect(loggedInAs).toBeUndefined();
685
+ _b = expect;
686
+ return [4 /*yield*/, userRow(userA.id)];
687
+ case 5:
688
+ _b.apply(void 0, [(_c.sent()).status]).toBe('deactivated');
689
+ return [2 /*return*/];
690
+ }
691
+ });
692
+ }); });
693
+ it('login refuses a staff-deactivated account outright', function () { return __awaiter(void 0, void 0, void 0, function () {
694
+ var _a, loggedInAs, sent;
695
+ return __generator(this, function (_b) {
696
+ switch (_b.label) {
697
+ case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().update(user_1.tables.User, { id: userB.id, status: 'deactivated' })];
698
+ case 1:
699
+ _b.sent();
700
+ return [4 /*yield*/, invokeLogin(userB.email, PASSWORD_B)];
701
+ case 2:
702
+ _a = _b.sent(), loggedInAs = _a.loggedInAs, sent = _a.sent;
703
+ expect(sent).toEqual({ error: 'This account has been deactivated' });
704
+ expect(loggedInAs).toBeUndefined();
705
+ return [2 /*return*/];
706
+ }
707
+ });
708
+ }); });
709
+ });
710
+ //# sourceMappingURL=AccountDeletion.integration.test.js.map