@proteinjs/user-server 1.12.0 → 1.14.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.
- package/CHANGELOG.md +22 -0
- package/dist/src/authorization/UserActivityStamp.d.ts +32 -0
- package/dist/src/authorization/UserActivityStamp.d.ts.map +1 -0
- package/dist/src/authorization/UserActivityStamp.js +126 -0
- package/dist/src/authorization/UserActivityStamp.js.map +1 -0
- package/dist/src/authorization/userCache.d.ts.map +1 -1
- package/dist/src/authorization/userCache.js +9 -0
- package/dist/src/authorization/userCache.js.map +1 -1
- package/dist/test/SharedScopeEncryption.integration.test.d.ts +2 -0
- package/dist/test/SharedScopeEncryption.integration.test.d.ts.map +1 -0
- package/dist/test/SharedScopeEncryption.integration.test.js +571 -0
- package/dist/test/SharedScopeEncryption.integration.test.js.map +1 -0
- package/dist/test/UserActivityStamp.integration.test.d.ts +2 -0
- package/dist/test/UserActivityStamp.integration.test.d.ts.map +1 -0
- package/dist/test/UserActivityStamp.integration.test.js +255 -0
- package/dist/test/UserActivityStamp.integration.test.js.map +1 -0
- package/package.json +4 -4
- package/src/authorization/UserActivityStamp.ts +66 -0
- package/src/authorization/userCache.ts +9 -0
- package/test/SharedScopeEncryption.integration.test.ts +363 -0
- package/test/UserActivityStamp.integration.test.ts +122 -0
|
@@ -0,0 +1,255 @@
|
|
|
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 __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
var moment_1 = __importDefault(require("moment"));
|
|
43
|
+
var db_1 = require("@proteinjs/db");
|
|
44
|
+
var user_1 = require("@proteinjs/user");
|
|
45
|
+
var userCache_1 = require("../src/authorization/userCache");
|
|
46
|
+
var UserActivityStamp_1 = require("../src/authorization/UserActivityStamp");
|
|
47
|
+
var UserServerTestEnvironment_1 = require("./UserServerTestEnvironment");
|
|
48
|
+
var testEnv = new UserServerTestEnvironment_1.UserServerTestEnvironment();
|
|
49
|
+
var stampInternals = function (stamp) { return stamp.constructor; };
|
|
50
|
+
var activityRows = function (scope) { return __awaiter(void 0, void 0, void 0, function () { return __generator(this, function (_a) {
|
|
51
|
+
switch (_a.label) {
|
|
52
|
+
case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(user_1.tables.UserActivity, { scope: scope })];
|
|
53
|
+
case 1: return [2 /*return*/, _a.sent()];
|
|
54
|
+
}
|
|
55
|
+
}); }); };
|
|
56
|
+
/** The stamp write is fire-and-forget off userCache — poll the OUTCOME (row present) briefly. */
|
|
57
|
+
var waitForActivityRow = function (scope) { return __awaiter(void 0, void 0, void 0, function () {
|
|
58
|
+
var attempt, rows;
|
|
59
|
+
return __generator(this, function (_a) {
|
|
60
|
+
switch (_a.label) {
|
|
61
|
+
case 0:
|
|
62
|
+
attempt = 0;
|
|
63
|
+
_a.label = 1;
|
|
64
|
+
case 1:
|
|
65
|
+
if (!(attempt < 50)) return [3 /*break*/, 5];
|
|
66
|
+
return [4 /*yield*/, activityRows(scope)];
|
|
67
|
+
case 2:
|
|
68
|
+
rows = _a.sent();
|
|
69
|
+
if (rows.length > 0) {
|
|
70
|
+
return [2 /*return*/, rows[0]];
|
|
71
|
+
}
|
|
72
|
+
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 100); })];
|
|
73
|
+
case 3:
|
|
74
|
+
_a.sent();
|
|
75
|
+
_a.label = 4;
|
|
76
|
+
case 4:
|
|
77
|
+
attempt++;
|
|
78
|
+
return [3 /*break*/, 1];
|
|
79
|
+
case 5: throw new Error("No user_activity row appeared for scope ".concat(scope));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}); };
|
|
83
|
+
/**
|
|
84
|
+
* LAST ACTIVITY = HUMAN PRESENCE (UserActivityTable's contract): the stamp fires from
|
|
85
|
+
* userCache.create — the once-per-interactive-request session-cache build — and from nowhere
|
|
86
|
+
* else. These tests pin the seam's outcomes: a human's request lands the presence row; machine
|
|
87
|
+
* accounts (real sessions, e.g. the error bridge's polling login) never do; repeated stamps
|
|
88
|
+
* keep ONE row per user; the throttle holds writes to the interval.
|
|
89
|
+
*/
|
|
90
|
+
describe('UserActivityStamp — interactive presence', function () {
|
|
91
|
+
beforeAll(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
92
|
+
return __generator(this, function (_a) {
|
|
93
|
+
switch (_a.label) {
|
|
94
|
+
case 0: return [4 /*yield*/, testEnv.beforeAll()];
|
|
95
|
+
case 1:
|
|
96
|
+
_a.sent();
|
|
97
|
+
return [2 /*return*/];
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}); });
|
|
101
|
+
afterAll(function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
102
|
+
return __generator(this, function (_a) {
|
|
103
|
+
switch (_a.label) {
|
|
104
|
+
case 0: return [4 /*yield*/, testEnv.afterAll()];
|
|
105
|
+
case 1:
|
|
106
|
+
_a.sent();
|
|
107
|
+
return [2 /*return*/];
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}); });
|
|
111
|
+
beforeEach(function () {
|
|
112
|
+
// Each test controls its own throttle history.
|
|
113
|
+
stampInternals(new UserActivityStamp_1.UserActivityStamp()).lastStampMs.clear();
|
|
114
|
+
});
|
|
115
|
+
it('stamps presence when a signed-in human makes an interactive request (through userCache.create)', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
116
|
+
var user, resolved, row;
|
|
117
|
+
return __generator(this, function (_a) {
|
|
118
|
+
switch (_a.label) {
|
|
119
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Present Human', email: 'present-human@test.local' })];
|
|
120
|
+
case 1:
|
|
121
|
+
user = _a.sent();
|
|
122
|
+
return [4 /*yield*/, userCache_1.userCache.create('interactive-session', user.email)];
|
|
123
|
+
case 2:
|
|
124
|
+
resolved = _a.sent();
|
|
125
|
+
expect(resolved.id).toBe(user.id);
|
|
126
|
+
return [4 /*yield*/, waitForActivityRow(user.id)];
|
|
127
|
+
case 3:
|
|
128
|
+
row = _a.sent();
|
|
129
|
+
expect((0, moment_1.default)(row.lastActiveAt).isAfter((0, moment_1.default)().subtract(1, 'minute'))).toBe(true);
|
|
130
|
+
return [2 /*return*/];
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}); });
|
|
134
|
+
it('keeps ONE row per user and advances it on later stamps (scope-unique invariant)', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
135
|
+
var user, stamp, first, rows;
|
|
136
|
+
return __generator(this, function (_a) {
|
|
137
|
+
switch (_a.label) {
|
|
138
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Returning Human', email: 'returning-human@test.local' })];
|
|
139
|
+
case 1:
|
|
140
|
+
user = _a.sent();
|
|
141
|
+
stamp = new UserActivityStamp_1.UserActivityStamp();
|
|
142
|
+
return [4 /*yield*/, stamp.recordInteractiveRequest(user)];
|
|
143
|
+
case 2:
|
|
144
|
+
_a.sent();
|
|
145
|
+
return [4 /*yield*/, waitForActivityRow(user.id)];
|
|
146
|
+
case 3:
|
|
147
|
+
first = _a.sent();
|
|
148
|
+
// Clear the throttle so the second request stamps immediately.
|
|
149
|
+
stampInternals(stamp).lastStampMs.clear();
|
|
150
|
+
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 5); })];
|
|
151
|
+
case 4:
|
|
152
|
+
_a.sent();
|
|
153
|
+
return [4 /*yield*/, stamp.recordInteractiveRequest(user)];
|
|
154
|
+
case 5:
|
|
155
|
+
_a.sent();
|
|
156
|
+
return [4 /*yield*/, activityRows(user.id)];
|
|
157
|
+
case 6:
|
|
158
|
+
rows = _a.sent();
|
|
159
|
+
expect(rows).toHaveLength(1);
|
|
160
|
+
expect((0, moment_1.default)(rows[0].lastActiveAt).valueOf()).toBeGreaterThanOrEqual((0, moment_1.default)(first.lastActiveAt).valueOf());
|
|
161
|
+
return [2 /*return*/];
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}); });
|
|
165
|
+
it('never stamps a machine account, even though its requests ride a real session', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
166
|
+
var machine, resolved, _a;
|
|
167
|
+
return __generator(this, function (_b) {
|
|
168
|
+
switch (_b.label) {
|
|
169
|
+
case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().insert(user_1.tables.User, {
|
|
170
|
+
name: 'Ops machine',
|
|
171
|
+
email: 'stamp-machine@test.local',
|
|
172
|
+
password: 'test',
|
|
173
|
+
emailVerified: true,
|
|
174
|
+
roles: [],
|
|
175
|
+
isLoadedFromSource: true,
|
|
176
|
+
})];
|
|
177
|
+
case 1:
|
|
178
|
+
machine = _b.sent();
|
|
179
|
+
// Await the stamp DIRECTLY (deterministic absence — no fire-and-forget race), then confirm
|
|
180
|
+
// the transport seam agrees by resolving the machine session through userCache too.
|
|
181
|
+
return [4 /*yield*/, new UserActivityStamp_1.UserActivityStamp().recordInteractiveRequest(machine)];
|
|
182
|
+
case 2:
|
|
183
|
+
// Await the stamp DIRECTLY (deterministic absence — no fire-and-forget race), then confirm
|
|
184
|
+
// the transport seam agrees by resolving the machine session through userCache too.
|
|
185
|
+
_b.sent();
|
|
186
|
+
return [4 /*yield*/, userCache_1.userCache.create('machine-session', machine.email)];
|
|
187
|
+
case 3:
|
|
188
|
+
resolved = _b.sent();
|
|
189
|
+
expect(resolved.id).toBe(machine.id);
|
|
190
|
+
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 300); })];
|
|
191
|
+
case 4:
|
|
192
|
+
_b.sent();
|
|
193
|
+
_a = expect;
|
|
194
|
+
return [4 /*yield*/, activityRows(machine.id)];
|
|
195
|
+
case 5:
|
|
196
|
+
_a.apply(void 0, [_b.sent()]).toHaveLength(0);
|
|
197
|
+
return [2 /*return*/];
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}); });
|
|
201
|
+
it('throttles: a second request inside the interval writes nothing', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
202
|
+
var user, stamp, first, rows;
|
|
203
|
+
return __generator(this, function (_a) {
|
|
204
|
+
switch (_a.label) {
|
|
205
|
+
case 0: return [4 /*yield*/, testEnv.createUser({ name: 'Rapid Human', email: 'rapid-human@test.local' })];
|
|
206
|
+
case 1:
|
|
207
|
+
user = _a.sent();
|
|
208
|
+
stamp = new UserActivityStamp_1.UserActivityStamp();
|
|
209
|
+
return [4 /*yield*/, stamp.recordInteractiveRequest(user)];
|
|
210
|
+
case 2:
|
|
211
|
+
_a.sent();
|
|
212
|
+
return [4 /*yield*/, waitForActivityRow(user.id)];
|
|
213
|
+
case 3:
|
|
214
|
+
first = _a.sent();
|
|
215
|
+
// Throttle history now holds this user; a second stamp inside the interval must not write.
|
|
216
|
+
return [4 /*yield*/, stamp.recordInteractiveRequest(user)];
|
|
217
|
+
case 4:
|
|
218
|
+
// Throttle history now holds this user; a second stamp inside the interval must not write.
|
|
219
|
+
_a.sent();
|
|
220
|
+
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 300); })];
|
|
221
|
+
case 5:
|
|
222
|
+
_a.sent();
|
|
223
|
+
return [4 /*yield*/, activityRows(user.id)];
|
|
224
|
+
case 6:
|
|
225
|
+
rows = _a.sent();
|
|
226
|
+
expect(rows).toHaveLength(1);
|
|
227
|
+
expect((0, moment_1.default)(rows[0].lastActiveAt).valueOf()).toBe((0, moment_1.default)(first.lastActiveAt).valueOf());
|
|
228
|
+
return [2 /*return*/];
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}); });
|
|
232
|
+
it('a session for a missing account still resolves guest and stamps nothing (no throw)', function () { return __awaiter(void 0, void 0, void 0, function () {
|
|
233
|
+
var before, resolved, _a;
|
|
234
|
+
return __generator(this, function (_b) {
|
|
235
|
+
switch (_b.label) {
|
|
236
|
+
case 0: return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(user_1.tables.UserActivity, {})];
|
|
237
|
+
case 1:
|
|
238
|
+
before = (_b.sent()).length;
|
|
239
|
+
return [4 /*yield*/, userCache_1.userCache.create('stale-session', 'no-such-account@test.local')];
|
|
240
|
+
case 2:
|
|
241
|
+
resolved = _b.sent();
|
|
242
|
+
expect(resolved.id).toBe('guest');
|
|
243
|
+
return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, 200); })];
|
|
244
|
+
case 3:
|
|
245
|
+
_b.sent();
|
|
246
|
+
_a = expect;
|
|
247
|
+
return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(user_1.tables.UserActivity, {})];
|
|
248
|
+
case 4:
|
|
249
|
+
_a.apply(void 0, [(_b.sent()).length]).toBe(before);
|
|
250
|
+
return [2 /*return*/];
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
}); });
|
|
254
|
+
});
|
|
255
|
+
//# sourceMappingURL=UserActivityStamp.integration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserActivityStamp.integration.test.js","sourceRoot":"","sources":["../../test/UserActivityStamp.integration.test.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,kDAA4B;AAC5B,oCAA8C;AAC9C,wCAAuE;AACvE,4DAA2D;AAC3D,4EAA2E;AAC3E,yEAAwE;AAExE,IAAM,OAAO,GAAG,IAAI,qDAAyB,EAAE,CAAC;AAMhD,IAAM,cAAc,GAAG,UAAC,KAAwB,IAAK,OAAC,KAAmC,CAAC,WAAW,EAAhD,CAAgD,CAAC;AAEtG,IAAM,YAAY,GAAG,UAAO,KAAa;;gBACvC,qBAAM,IAAA,kBAAa,GAAE,CAAC,KAAK,CAAC,aAAM,CAAC,YAAY,EAAE,EAAE,KAAK,OAAA,EAAE,CAAC,EAAA;gBAA3D,sBAAA,SAA2D,EAAA;;SAAA,CAAC;AAE9D,iGAAiG;AACjG,IAAM,kBAAkB,GAAG,UAAO,KAAa;;;;;gBACpC,OAAO,GAAG,CAAC;;;qBAAE,CAAA,OAAO,GAAG,EAAE,CAAA;gBACnB,qBAAM,YAAY,CAAC,KAAK,CAAC,EAAA;;gBAAhC,IAAI,GAAG,SAAyB;gBACtC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE;oBACnB,sBAAO,IAAI,CAAC,CAAC,CAAC,EAAC;iBAChB;gBACD,qBAAM,IAAI,OAAO,CAAC,UAAC,OAAO,IAAK,OAAA,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,EAAxB,CAAwB,CAAC,EAAA;;gBAAxD,SAAwD,CAAC;;;gBALvB,OAAO,EAAE,CAAA;;oBAO7C,MAAM,IAAI,KAAK,CAAC,kDAA2C,KAAK,CAAE,CAAC,CAAC;;;KACrE,CAAC;AAEF;;;;;;GAMG;AACH,QAAQ,CAAC,0CAA0C,EAAE;IACnD,SAAS,CAAC;;;wBACR,qBAAM,OAAO,CAAC,SAAS,EAAE,EAAA;;oBAAzB,SAAyB,CAAC;;;;SAC3B,CAAC,CAAC;IAEH,QAAQ,CAAC;;;wBACP,qBAAM,OAAO,CAAC,QAAQ,EAAE,EAAA;;oBAAxB,SAAwB,CAAC;;;;SAC1B,CAAC,CAAC;IAEH,UAAU,CAAC;QACT,+CAA+C;QAC/C,cAAc,CAAC,IAAI,qCAAiB,EAAE,CAAC,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;IAC9D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gGAAgG,EAAE;;;;wBACtF,qBAAM,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,EAAA;;oBAA7F,IAAI,GAAG,SAAsF;oBAElF,qBAAM,qBAAS,CAAC,MAAM,CAAC,qBAAqB,EAAE,IAAI,CAAC,KAAK,CAAC,EAAA;;oBAApE,QAAQ,GAAG,SAAyD;oBAC1E,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;oBAEtB,qBAAM,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAAvC,GAAG,GAAG,SAAiC;oBAC7C,MAAM,CAAC,IAAA,gBAAM,EAAC,GAAG,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,IAAA,gBAAM,GAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;;;;SACrF,CAAC,CAAC;IAEH,EAAE,CAAC,iFAAiF,EAAE;;;;wBACvE,qBAAM,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,iBAAiB,EAAE,KAAK,EAAE,4BAA4B,EAAE,CAAC,EAAA;;oBAAjG,IAAI,GAAG,SAA0F;oBACjG,KAAK,GAAG,IAAI,qCAAiB,EAAE,CAAC;oBAEtC,qBAAM,KAAK,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAA;;oBAA1C,SAA0C,CAAC;oBAC7B,qBAAM,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAAzC,KAAK,GAAG,SAAiC;oBAE/C,+DAA+D;oBAC/D,cAAc,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC;oBAC1C,qBAAM,IAAI,OAAO,CAAC,UAAC,OAAO,IAAK,OAAA,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,EAAtB,CAAsB,CAAC,EAAA;;oBAAtD,SAAsD,CAAC;oBACvD,qBAAM,KAAK,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAA;;oBAA1C,SAA0C,CAAC;oBAE9B,qBAAM,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAAlC,IAAI,GAAG,SAA2B;oBACxC,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;oBAC7B,MAAM,CAAC,IAAA,gBAAM,EAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,sBAAsB,CAAC,IAAA,gBAAM,EAAC,KAAK,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;;;;SAC7G,CAAC,CAAC;IAEH,EAAE,CAAC,8EAA8E,EAAE;;;;wBACjE,qBAAM,IAAA,kBAAa,GAAE,CAAC,MAAM,CAAC,aAAM,CAAC,IAAI,EAAE;wBACxD,IAAI,EAAE,aAAa;wBACnB,KAAK,EAAE,0BAA0B;wBACjC,QAAQ,EAAE,MAAM;wBAChB,aAAa,EAAE,IAAI;wBACnB,KAAK,EAAE,EAAE;wBACT,kBAAkB,EAAE,IAAI;qBACN,CAAC,EAAA;;oBAPf,OAAO,GAAG,SAOK;oBAErB,2FAA2F;oBAC3F,oFAAoF;oBACpF,qBAAM,IAAI,qCAAiB,EAAE,CAAC,wBAAwB,CAAC,OAAO,CAAC,EAAA;;oBAF/D,2FAA2F;oBAC3F,oFAAoF;oBACpF,SAA+D,CAAC;oBAC/C,qBAAM,qBAAS,CAAC,MAAM,CAAC,iBAAiB,EAAE,OAAO,CAAC,KAAK,CAAC,EAAA;;oBAAnE,QAAQ,GAAG,SAAwD;oBACzE,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;oBACrC,qBAAM,IAAI,OAAO,CAAC,UAAC,OAAO,IAAK,OAAA,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,EAAxB,CAAwB,CAAC,EAAA;;oBAAxD,SAAwD,CAAC;oBAEzD,KAAA,MAAM,CAAA;oBAAC,qBAAM,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,EAAA;;oBAArC,kBAAO,SAA8B,EAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;;;;SACxD,CAAC,CAAC;IAEH,EAAE,CAAC,gEAAgE,EAAE;;;;wBACtD,qBAAM,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC,EAAA;;oBAAzF,IAAI,GAAG,SAAkF;oBACzF,KAAK,GAAG,IAAI,qCAAiB,EAAE,CAAC;oBAEtC,qBAAM,KAAK,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAA;;oBAA1C,SAA0C,CAAC;oBAC7B,qBAAM,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAAzC,KAAK,GAAG,SAAiC;oBAE/C,2FAA2F;oBAC3F,qBAAM,KAAK,CAAC,wBAAwB,CAAC,IAAI,CAAC,EAAA;;oBAD1C,2FAA2F;oBAC3F,SAA0C,CAAC;oBAC3C,qBAAM,IAAI,OAAO,CAAC,UAAC,OAAO,IAAK,OAAA,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,EAAxB,CAAwB,CAAC,EAAA;;oBAAxD,SAAwD,CAAC;oBAE5C,qBAAM,YAAY,CAAC,IAAI,CAAC,EAAE,CAAC,EAAA;;oBAAlC,IAAI,GAAG,SAA2B;oBACxC,MAAM,CAAC,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;oBAC7B,MAAM,CAAC,IAAA,gBAAM,EAAC,IAAI,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,IAAA,gBAAM,EAAC,KAAK,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;;;;SAC3F,CAAC,CAAC;IAEH,EAAE,CAAC,oFAAoF,EAAE;;;;wBACvE,qBAAM,IAAA,kBAAa,GAAE,CAAC,KAAK,CAAC,aAAM,CAAC,YAAY,EAAE,EAAE,CAAC,EAAA;;oBAA9D,MAAM,GAAG,CAAC,SAAoD,CAAC,CAAC,MAAM;oBAC3D,qBAAM,qBAAS,CAAC,MAAM,CAAC,eAAe,EAAE,4BAA4B,CAAC,EAAA;;oBAAhF,QAAQ,GAAG,SAAqE;oBACtF,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;oBAClC,qBAAM,IAAI,OAAO,CAAC,UAAC,OAAO,IAAK,OAAA,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,EAAxB,CAAwB,CAAC,EAAA;;oBAAxD,SAAwD,CAAC;oBACzD,KAAA,MAAM,CAAA;oBAAE,qBAAM,IAAA,kBAAa,GAAE,CAAC,KAAK,CAAC,aAAM,CAAC,YAAY,EAAE,EAAE,CAAC,EAAA;;oBAA5D,kBAAO,CAAC,SAAoD,CAAC,CAAC,MAAM,EAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;;;;SACpF,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/user-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "User server components",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -22,14 +22,14 @@
|
|
|
22
22
|
"test": "jest --runInBand --passWithNoTests"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@proteinjs/db": "^1.
|
|
25
|
+
"@proteinjs/db": "^1.37.0",
|
|
26
26
|
"@proteinjs/db-file": "^1.5.3",
|
|
27
27
|
"@proteinjs/email-server": "^1.3.0",
|
|
28
28
|
"@proteinjs/logger": "^1.0.21",
|
|
29
29
|
"@proteinjs/reflection": "^1.2.0",
|
|
30
30
|
"@proteinjs/server": "^3.5.1",
|
|
31
31
|
"@proteinjs/server-api": "^3.0.11",
|
|
32
|
-
"@proteinjs/user": "^1.
|
|
32
|
+
"@proteinjs/user": "^1.15.0",
|
|
33
33
|
"@proteinjs/util": "^1.6.1",
|
|
34
34
|
"argon2": "^0.45.1",
|
|
35
35
|
"crypto-js": "4.0.0",
|
|
@@ -59,5 +59,5 @@
|
|
|
59
59
|
},
|
|
60
60
|
"main": "./dist/generated/index.js",
|
|
61
61
|
"types": "./dist/generated/index.d.ts",
|
|
62
|
-
"gitHead": "
|
|
62
|
+
"gitHead": "2f490c4aa828ed0a241421fb01d9fd372d88e4a2"
|
|
63
63
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import moment from 'moment';
|
|
2
|
+
import { getDbAsSystem } from '@proteinjs/db';
|
|
3
|
+
import { Logger } from '@proteinjs/logger';
|
|
4
|
+
import { tables, type User, type UserActivity } from '@proteinjs/user';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Writes the LAST-ACTIVITY presence stamp (`user_activity`, one row per user — see
|
|
8
|
+
* UserActivityTable's contract): "this human made an interactive request just now".
|
|
9
|
+
*
|
|
10
|
+
* The TRANSPORT keying lives at the call site — userCache.create runs once per session-cookie
|
|
11
|
+
* request (wrapRoute's session-cache build) and only there, so seeded/background contexts
|
|
12
|
+
* (runInUserScope sets session data directly) structurally never reach this class. What this
|
|
13
|
+
* class owns is the ACCOUNT predicate: machine accounts (`isLoadedFromSource`) are refused —
|
|
14
|
+
* their requests do arrive over real sessions (e.g. the error bridge's polling), but a machine
|
|
15
|
+
* logging in is not a person being present.
|
|
16
|
+
*
|
|
17
|
+
* Write behavior mirrors DbSessionStore's touch: throttled per user (a presence fact consumed at
|
|
18
|
+
* day grain needs no finer cadence, and an unthrottled stamp would put a write on EVERY request),
|
|
19
|
+
* fail-open (a lost stamp is a few minutes of staleness, never a failed request — the returned
|
|
20
|
+
* promise NEVER rejects), and race-tolerant (concurrent first stamps contend on the scope-unique
|
|
21
|
+
* index; the loser's error is swallowed as debug).
|
|
22
|
+
*/
|
|
23
|
+
export class UserActivityStamp {
|
|
24
|
+
/** Stamp at most this often per user — same cadence class as DbSessionStore.TOUCH_INTERVAL. */
|
|
25
|
+
private static readonly STAMP_INTERVAL_MS = 1000 * 60 * 5;
|
|
26
|
+
/** Process-wide: userCache is a plain object, so throttle state can't live per-instance. */
|
|
27
|
+
private static lastStampMs = new Map<string, number>();
|
|
28
|
+
|
|
29
|
+
private logger = new Logger({ name: this.constructor.name });
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Record that `user` is present on an interactive request. Fire-and-forget safe: errors are
|
|
33
|
+
* handled (and logged) here, so callers may `void` the returned promise.
|
|
34
|
+
*/
|
|
35
|
+
recordInteractiveRequest(user: User): Promise<void> {
|
|
36
|
+
if (!user.id || user.isLoadedFromSource === true) {
|
|
37
|
+
return Promise.resolve();
|
|
38
|
+
}
|
|
39
|
+
const last = UserActivityStamp.lastStampMs.get(user.id) ?? 0;
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (now - last < UserActivityStamp.STAMP_INTERVAL_MS) {
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
}
|
|
44
|
+
UserActivityStamp.lastStampMs.set(user.id, now);
|
|
45
|
+
if (UserActivityStamp.lastStampMs.size > 10000) {
|
|
46
|
+
UserActivityStamp.lastStampMs.clear(); // bounded memory; worst case is one extra stamp per user
|
|
47
|
+
}
|
|
48
|
+
return this.upsert(user.id).catch((error) => {
|
|
49
|
+
// Contention (a concurrent request stamped first, racing the scope-unique index) and real
|
|
50
|
+
// failures land here alike; both are harmless to the request. Un-throttle so the next
|
|
51
|
+
// request retries instead of waiting out a full interval on a stamp that never landed.
|
|
52
|
+
UserActivityStamp.lastStampMs.delete(user.id);
|
|
53
|
+
this.logger.error({ message: 'Failed to write user activity stamp', error });
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private async upsert(userId: string): Promise<void> {
|
|
58
|
+
const db = getDbAsSystem();
|
|
59
|
+
const existing = await db.get(tables.UserActivity, { scope: userId });
|
|
60
|
+
if (existing) {
|
|
61
|
+
await db.update(tables.UserActivity, { id: existing.id, lastActiveAt: moment() } as UserActivity);
|
|
62
|
+
} else {
|
|
63
|
+
await db.insert(tables.UserActivity, { scope: userId, lastActiveAt: moment() } as UserActivity);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -4,8 +4,10 @@ import { getDbAsSystem } from '@proteinjs/db';
|
|
|
4
4
|
import { Logger } from '@proteinjs/logger';
|
|
5
5
|
import { User, tables, guestUser, USER_SESSION_CACHE_KEY } from '@proteinjs/user';
|
|
6
6
|
import { DefaultAdminCredentials } from '../authentication/DefaultAdminCredentials';
|
|
7
|
+
import { UserActivityStamp } from './UserActivityStamp';
|
|
7
8
|
|
|
8
9
|
const logger = new Logger({ name: 'userCache' });
|
|
10
|
+
const userActivityStamp = new UserActivityStamp();
|
|
9
11
|
|
|
10
12
|
export const userCache: SessionDataCache<User> = {
|
|
11
13
|
key: USER_SESSION_CACHE_KEY,
|
|
@@ -38,6 +40,13 @@ export const userCache: SessionDataCache<User> = {
|
|
|
38
40
|
} else if (accountUser) {
|
|
39
41
|
delete (accountUser as any)['password'];
|
|
40
42
|
user = accountUser;
|
|
43
|
+
// LAST ACTIVITY (human presence — UserActivityTable's contract): stamped HERE because
|
|
44
|
+
// this cache build runs exactly once per session-cookie request (wrapRoute), i.e. only
|
|
45
|
+
// for interactive transport. Background/seeded contexts (runInUserScope) set session
|
|
46
|
+
// data directly and never pass through, so machinery acting as the user structurally
|
|
47
|
+
// cannot stamp; the stamp itself refuses machine accounts. Fire-and-forget: the
|
|
48
|
+
// returned promise never rejects, and a request never waits on its own stamp.
|
|
49
|
+
void userActivityStamp.recordInteractiveRequest(user);
|
|
41
50
|
} else {
|
|
42
51
|
// A session can outlive its account (row deleted, or a dev auto-login for a never-created
|
|
43
52
|
// email). Resolve it to the unauthenticated guest session — the client sees no
|