@proteinjs/user-server 1.19.0 → 1.20.1

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 (36) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/generated/index.js +1 -1
  3. package/dist/generated/index.js.map +1 -1
  4. package/dist/src/authorization/UserActivityStamp.d.ts +3 -3
  5. package/dist/src/authorization/UserActivityStamp.js +3 -3
  6. package/dist/src/routes/DevBootstrapRoles.d.ts +21 -0
  7. package/dist/src/routes/DevBootstrapRoles.d.ts.map +1 -0
  8. package/dist/src/routes/DevBootstrapRoles.js +64 -0
  9. package/dist/src/routes/DevBootstrapRoles.js.map +1 -0
  10. package/dist/src/routes/devLogin.d.ts +11 -0
  11. package/dist/src/routes/devLogin.d.ts.map +1 -1
  12. package/dist/src/routes/devLogin.js +23 -3
  13. package/dist/src/routes/devLogin.js.map +1 -1
  14. package/dist/src/services/Roles.d.ts +29 -1
  15. package/dist/src/services/Roles.d.ts.map +1 -1
  16. package/dist/src/services/Roles.js +88 -14
  17. package/dist/src/services/Roles.js.map +1 -1
  18. package/dist/test/DevLogin.test.js +5 -2
  19. package/dist/test/DevLogin.test.js.map +1 -1
  20. package/dist/test/DevLoginBootstrapAdmin.test.js +2 -0
  21. package/dist/test/DevLoginBootstrapAdmin.test.js.map +1 -1
  22. package/dist/test/DevLoginBootstrapRoles.test.d.ts +2 -0
  23. package/dist/test/DevLoginBootstrapRoles.test.d.ts.map +1 -0
  24. package/dist/test/DevLoginBootstrapRoles.test.js +487 -0
  25. package/dist/test/DevLoginBootstrapRoles.test.js.map +1 -0
  26. package/dist/test/UserCacheDoesNotStampPresence.integration.test.js +2 -2
  27. package/generated/index.ts +1 -1
  28. package/package.json +3 -3
  29. package/src/authorization/UserActivityStamp.ts +3 -3
  30. package/src/routes/DevBootstrapRoles.ts +50 -0
  31. package/src/routes/devLogin.ts +18 -0
  32. package/src/services/Roles.ts +61 -1
  33. package/test/DevLogin.test.ts +5 -2
  34. package/test/DevLoginBootstrapAdmin.test.ts +2 -0
  35. package/test/DevLoginBootstrapRoles.test.ts +248 -0
  36. package/test/UserCacheDoesNotStampPresence.integration.test.ts +2 -2
@@ -11,12 +11,16 @@ import { Service } from '@proteinjs/service';
11
11
  *
12
12
  * Break-glass roles are never granted through the service door — see `changeRole`; revoking one
13
13
  * stays allowed. The ONE break-glass grant in code is `bootstrapAdmin`, the dev first-admin door
14
- * (server-internal; reachable only through `/dev/login`'s gates, never over RPC).
14
+ * (server-internal; reachable only through `/dev/login`'s gates, never over RPC). Its sibling
15
+ * `bootstrapRoles` is the dev role-bootstrap door's grant — never break-glass, catalog-checked.
15
16
  *
16
17
  * Nobody edits their OWN roles: separating 'roles' from 'users' means nothing if the holder can
17
18
  * simply grant themselves more, and a self-revoke is the mirror hazard (the last holder locking
18
19
  * themselves out). Both directions are refused — see `changeRole`; ask another user manager.
19
20
  */
21
+ /** What one `bootstrapRoles` call did, per role: written now, already on the row, or refused with the reason. */
22
+ export type BootstrapRolesOutcome = { granted: string[]; held: string[]; refused: { role: string; why: string }[] };
23
+
20
24
  export class Roles implements RolesService {
21
25
  public serviceMetadata: Service['serviceMetadata'] = {
22
26
  auth: {
@@ -67,6 +71,62 @@ export class Roles implements RolesService {
67
71
  return 'granted';
68
72
  }
69
73
 
74
+ /**
75
+ * The dev role-bootstrap door's grant — `/dev/login` honoring `DEV_BOOTSTRAP_ROLES`
76
+ * (routes/devLogin.ts, the grammar in routes/DevBootstrapRoles.ts). A fresh development
77
+ * database has one admin at most (the first-admin door) and every other account role-less, and
78
+ * a consumer's `adminGrantOnly` roles can be handed out by an admin only — a manual act on
79
+ * every fresh database. So this grants `roles` to `email`'s account, each
80
+ * once: a role the account holds is `held` (no write, no audit row — idempotent); nothing is
81
+ * ever revoked; a role the catalog does not know or a break-glass role is `refused` and named
82
+ * (`bootstrapAdmin` is the ONE break-glass path, with its own rail). Admin-grant-only roles ARE
83
+ * granted here — that is the door's point. Every grant is audited like any grant, actor = the
84
+ * account itself (the door acts for nobody else), the role update and its rows in one
85
+ * transaction.
86
+ *
87
+ * Server-internal, like `bootstrapAdmin`: absent from `RolesService`, never RPC-reachable; the
88
+ * caller's two gates (DEVELOPMENT + DEV_AUTO_LOGIN_EMAIL) are the only way in, and a deployment
89
+ * outside development never sets the variable.
90
+ */
91
+ async bootstrapRoles(email: string, roles: string[]): Promise<BootstrapRolesOutcome> {
92
+ const logger = new Logger({ name: 'Roles.bootstrapRoles' });
93
+ const db = getDbAsSystem();
94
+ const user = await db.get(tables.User, { email: email.toLowerCase() });
95
+ if (!user) {
96
+ throw new Error(`bootstrapRoles: no account for ${email} — the door creates the account before it grants`);
97
+ }
98
+
99
+ const held = user.roles ?? [];
100
+ const outcome: BootstrapRolesOutcome = { granted: [], held: [], refused: [] };
101
+ for (const role of Array.from(new Set(roles))) {
102
+ const entry = RolesCatalog.getEntry(role);
103
+ if (!entry) {
104
+ outcome.refused.push({ role, why: 'unknown role' });
105
+ } else if (entry.breakGlass) {
106
+ outcome.refused.push({ role, why: 'break-glass' });
107
+ } else if (held.includes(role)) {
108
+ outcome.held.push(role);
109
+ } else {
110
+ outcome.granted.push(role);
111
+ }
112
+ }
113
+ if (outcome.granted.length === 0) {
114
+ return outcome;
115
+ }
116
+
117
+ await db.runTransaction(async () => {
118
+ await db.update(tables.User, { id: user.id, roles: [...held, ...outcome.granted] });
119
+ for (const role of outcome.granted) {
120
+ await db.insert(tables.RoleGrantEvent, { actor: user.id, target: user.id, role, action: 'grant' });
121
+ }
122
+ });
123
+ logger.info({
124
+ message: 'Roles granted by the dev role-bootstrap door',
125
+ obj: { target: user.id, email, granted: outcome.granted },
126
+ });
127
+ return outcome;
128
+ }
129
+
70
130
  private async changeRole(userId: string, role: string, action: 'grant' | 'revoke'): Promise<void> {
71
131
  const logger = new Logger({ name: `Roles.${action}Role` });
72
132
  const entry = RolesCatalog.getEntry(role);
@@ -14,8 +14,9 @@ const testEnv = new UserServerTestEnvironment();
14
14
  * - Double gate: `DEVELOPMENT` AND `DEV_AUTO_LOGIN_EMAIL` both present, else 404 (unchanged).
15
15
  * - Domain rail: a `?email=` param must share `DEV_AUTO_LOGIN_EMAIL`'s domain — even a dev server
16
16
  * must not mint sessions (much less accounts) for arbitrary domains. Others 400.
17
- * The first-admin door (`DEV_BOOTSTRAP_ADMIN_EMAIL`) has its own suite, DevLoginBootstrapAdmin.test.ts;
18
- * here the variable is unset, so every created account is role-less.
17
+ * The first-admin door (`DEV_BOOTSTRAP_ADMIN_EMAIL`) and the role-bootstrap door (`DEV_BOOTSTRAP_ROLES`)
18
+ * have their own suites (DevLoginBootstrapAdmin.test.ts, DevLoginBootstrapRoles.test.ts); here both
19
+ * variables are unset, so every created account is role-less.
19
20
  */
20
21
 
21
22
  const ENV_EMAIL = 'dev@test.local';
@@ -27,6 +28,7 @@ describe('devLogin route', () => {
27
28
  DEVELOPMENT: process.env.DEVELOPMENT,
28
29
  DEV_AUTO_LOGIN_EMAIL: process.env.DEV_AUTO_LOGIN_EMAIL,
29
30
  DEV_BOOTSTRAP_ADMIN_EMAIL: process.env.DEV_BOOTSTRAP_ADMIN_EMAIL,
31
+ DEV_BOOTSTRAP_ROLES: process.env.DEV_BOOTSTRAP_ROLES,
30
32
  };
31
33
 
32
34
  beforeAll(async () => {
@@ -41,6 +43,7 @@ describe('devLogin route', () => {
41
43
  process.env.DEVELOPMENT = 'true';
42
44
  process.env.DEV_AUTO_LOGIN_EMAIL = ENV_EMAIL;
43
45
  delete process.env.DEV_BOOTSTRAP_ADMIN_EMAIL;
46
+ delete process.env.DEV_BOOTSTRAP_ROLES;
44
47
  });
45
48
 
46
49
  afterEach(() => {
@@ -33,6 +33,7 @@ describe('devLogin — the DEV_BOOTSTRAP_ADMIN_EMAIL first-admin door', () => {
33
33
  DEVELOPMENT: process.env.DEVELOPMENT,
34
34
  DEV_AUTO_LOGIN_EMAIL: process.env.DEV_AUTO_LOGIN_EMAIL,
35
35
  DEV_BOOTSTRAP_ADMIN_EMAIL: process.env.DEV_BOOTSTRAP_ADMIN_EMAIL,
36
+ DEV_BOOTSTRAP_ROLES: process.env.DEV_BOOTSTRAP_ROLES,
36
37
  };
37
38
 
38
39
  beforeAll(async () => {
@@ -47,6 +48,7 @@ describe('devLogin — the DEV_BOOTSTRAP_ADMIN_EMAIL first-admin door', () => {
47
48
  process.env.DEVELOPMENT = 'true';
48
49
  process.env.DEV_AUTO_LOGIN_EMAIL = ENV_EMAIL;
49
50
  process.env.DEV_BOOTSTRAP_ADMIN_EMAIL = BOOTSTRAP_EMAIL;
51
+ delete process.env.DEV_BOOTSTRAP_ROLES; // the role-bootstrap door has its own suite; here it is closed
50
52
  // Every case starts from a fresh database: no accounts, no audit trail.
51
53
  const db = getDbAsSystem();
52
54
  await db.delete(tables.RoleGrantEvent, {});
@@ -0,0 +1,248 @@
1
+ import { getDbAsSystem } from '@proteinjs/db';
2
+ import { Logger } from '@proteinjs/logger';
3
+ import { SourceRepository } from '@proteinjs/reflection';
4
+ import { RoleCatalogEntry, tables } from '@proteinjs/user';
5
+ import { invokeDevLogin } from './devLoginHarness';
6
+ import { UserServerTestEnvironment } from './UserServerTestEnvironment';
7
+
8
+ const testEnv = new UserServerTestEnvironment();
9
+
10
+ /**
11
+ * `DEV_BOOTSTRAP_ROLES` — the dev role-bootstrap door INSIDE `/dev/login`. A fresh development
12
+ * database has one admin at most (the first-admin door) and every other account is role-less —
13
+ * so a consumer's `adminGrantOnly` roles needed a manual admin act on every fresh database. The
14
+ * same door, the same two gates, one more variable: `email:role[,role];email:role…` — on a hit
15
+ * whose resolved address is listed, the listed roles the account does not hold are granted, once
16
+ * each:
17
+ * - behind the door's existing two gates (DEVELOPMENT AND DEV_AUTO_LOGIN_EMAIL): closed = 404 as
18
+ * before, and the variable changes nothing;
19
+ * - only for the request whose resolved address equals a listed address exactly (case-normalized
20
+ * the way every account email is) — every other address is untouched;
21
+ * - idempotent: a role the account holds is reported `held`, never re-granted, never re-audited;
22
+ * nothing is EVER revoked (a role the list does not name stays);
23
+ * - admin-grant-only roles are granted (that is the point — a fresh development database has no
24
+ * admin to grant them); break-glass and roles the catalog does not know are REFUSED and named;
25
+ * - each grant is audited like any grant (a role_grant_event row; actor = the account itself);
26
+ * - the outcome is ONE marker line, `[dev-bootstrap] <email>: granted …; held …; refused …`, the
27
+ * line provisioning tooling reads back from the server log as its proof.
28
+ * Outcomes are asserted on the rows (roles, audit), the marker line on the logger (it IS the
29
+ * contract its readers parse).
30
+ */
31
+
32
+ const ENV_EMAIL = 'dev@test.local';
33
+ const TEAM_EMAIL = 'team@test.local';
34
+ const OPS_EMAIL = 'ops@test.local';
35
+ const ROLES_ENV = `${TEAM_EMAIL}:staff,dev;${OPS_EMAIL}:ops`;
36
+
37
+ type SourceRepositoryInternals = { objectCache: Record<string, unknown[]> };
38
+
39
+ const userRow = async (email: string) => await getDbAsSystem().get(tables.User, { email });
40
+ const auditRows = async () => await getDbAsSystem().query(tables.RoleGrantEvent, {});
41
+ const adminRows = async () =>
42
+ (await getDbAsSystem().query(tables.User, {})).filter((user) => (user.roles ?? []).includes('admin'));
43
+
44
+ describe('devLogin — the DEV_BOOTSTRAP_ROLES role-bootstrap door', () => {
45
+ const originalEnv = {
46
+ DEVELOPMENT: process.env.DEVELOPMENT,
47
+ DEV_AUTO_LOGIN_EMAIL: process.env.DEV_AUTO_LOGIN_EMAIL,
48
+ DEV_BOOTSTRAP_ADMIN_EMAIL: process.env.DEV_BOOTSTRAP_ADMIN_EMAIL,
49
+ DEV_BOOTSTRAP_ROLES: process.env.DEV_BOOTSTRAP_ROLES,
50
+ };
51
+ let infoSpy: jest.SpyInstance;
52
+ const markerLines = () =>
53
+ infoSpy.mock.calls
54
+ .map((call) => String((call[0] as { message?: string })?.message ?? ''))
55
+ .filter((message) => message.startsWith('[dev-bootstrap]'));
56
+
57
+ beforeAll(async () => {
58
+ await testEnv.beforeAll();
59
+ // A consumer's catalog shape: 'staff' is admin-grant-only, 'dev'/'ops' plain.
60
+ (SourceRepository.get() as unknown as SourceRepositoryInternals).objectCache['@proteinjs/user/RoleCatalogEntry'] = [
61
+ { role: 'staff', description: 'Staff member', adminGrantOnly: true } as RoleCatalogEntry,
62
+ { role: 'dev', description: 'Developer tooling' } as RoleCatalogEntry,
63
+ { role: 'ops', description: 'Operations' } as RoleCatalogEntry,
64
+ ];
65
+ });
66
+
67
+ afterAll(async () => {
68
+ await testEnv.afterAll();
69
+ });
70
+
71
+ beforeEach(async () => {
72
+ process.env.DEVELOPMENT = 'true';
73
+ process.env.DEV_AUTO_LOGIN_EMAIL = ENV_EMAIL;
74
+ delete process.env.DEV_BOOTSTRAP_ADMIN_EMAIL;
75
+ process.env.DEV_BOOTSTRAP_ROLES = ROLES_ENV;
76
+ infoSpy = jest.spyOn(Logger.prototype, 'info');
77
+ // Every case starts from a fresh database: no accounts, no audit trail.
78
+ const db = getDbAsSystem();
79
+ await db.delete(tables.RoleGrantEvent, {});
80
+ await db.delete(tables.User, {});
81
+ });
82
+
83
+ afterEach(() => {
84
+ infoSpy.mockRestore();
85
+ for (const [key, value] of Object.entries(originalEnv)) {
86
+ if (value === undefined) {
87
+ delete process.env[key];
88
+ } else {
89
+ process.env[key] = value;
90
+ }
91
+ }
92
+ });
93
+
94
+ it('a fresh database + a listed address: the account is created carrying the listed roles (the admin-grant-only one included), one audit row per role, logged in, and the marker line names the grants', async () => {
95
+ expect(await userRow(TEAM_EMAIL)).toBeUndefined();
96
+
97
+ const outcome = await invokeDevLogin({ email: TEAM_EMAIL });
98
+
99
+ expect(outcome.loggedInAs).toBe(TEAM_EMAIL);
100
+ expect(outcome.sessionSaved).toBe(true);
101
+ expect(outcome.redirect).toBe('/');
102
+ const team = await userRow(TEAM_EMAIL);
103
+ expect(team!.roles).toEqual(['staff', 'dev']);
104
+ const events = await auditRows();
105
+ expect(events.map((e) => ({ actor: e.actor, target: e.target, role: e.role, action: e.action }))).toEqual(
106
+ expect.arrayContaining([
107
+ { actor: team!.id, target: team!.id, role: 'staff', action: 'grant' },
108
+ { actor: team!.id, target: team!.id, role: 'dev', action: 'grant' },
109
+ ])
110
+ );
111
+ expect(events).toHaveLength(2);
112
+ expect(markerLines()).toEqual([`[dev-bootstrap] ${TEAM_EMAIL}: granted staff, dev; held (none); refused (none)`]);
113
+ });
114
+
115
+ it('a second hit for the same address writes nothing: the roles stand, the audit trail is unchanged, the marker line reports them held', async () => {
116
+ await invokeDevLogin({ email: TEAM_EMAIL });
117
+ infoSpy.mockClear();
118
+
119
+ const outcome = await invokeDevLogin({ email: TEAM_EMAIL });
120
+
121
+ expect(outcome.loggedInAs).toBe(TEAM_EMAIL);
122
+ expect((await userRow(TEAM_EMAIL))!.roles).toEqual(['staff', 'dev']);
123
+ expect(await auditRows()).toHaveLength(2);
124
+ expect(markerLines()).toEqual([`[dev-bootstrap] ${TEAM_EMAIL}: granted (none); held staff, dev; refused (none)`]);
125
+ });
126
+
127
+ it('an existing account holding one listed role and one unlisted role: only the missing role is granted, the unlisted one stays — the door never revokes', async () => {
128
+ await testEnv.createUser({ name: 'Team', email: TEAM_EMAIL, roles: ['dev', 'sessions'] });
129
+
130
+ await invokeDevLogin({ email: TEAM_EMAIL });
131
+
132
+ expect((await userRow(TEAM_EMAIL))!.roles).toEqual(['dev', 'sessions', 'staff']);
133
+ const events = await auditRows();
134
+ expect(events).toHaveLength(1);
135
+ expect(events[0]).toMatchObject({ role: 'staff', action: 'grant' });
136
+ expect(markerLines()).toEqual([`[dev-bootstrap] ${TEAM_EMAIL}: granted staff; held dev; refused (none)`]);
137
+ });
138
+
139
+ it('an address the list does not name: an ordinary account, no grant, no audit row, no marker line — and the listed addresses stay uncreated', async () => {
140
+ const outcome = await invokeDevLogin({ email: 'agent@test.local' });
141
+
142
+ expect(outcome.loggedInAs).toBe('agent@test.local');
143
+ expect((await userRow('agent@test.local'))!.roles).toEqual([]);
144
+ expect(await auditRows()).toHaveLength(0);
145
+ expect(markerLines()).toEqual([]);
146
+ expect(await userRow(TEAM_EMAIL)).toBeUndefined();
147
+ expect(await userRow(OPS_EMAIL)).toBeUndefined();
148
+ });
149
+
150
+ it('each listed address gets ITS roles only — the second entry never inherits the first', async () => {
151
+ await invokeDevLogin({ email: OPS_EMAIL });
152
+
153
+ expect((await userRow(OPS_EMAIL))!.roles).toEqual(['ops']);
154
+ expect(await auditRows()).toHaveLength(1);
155
+ expect(markerLines()).toEqual([`[dev-bootstrap] ${OPS_EMAIL}: granted ops; held (none); refused (none)`]);
156
+ });
157
+
158
+ it('the match is case-normalized like every account email, and the grammar tolerates whitespace around every token', async () => {
159
+ process.env.DEV_BOOTSTRAP_ROLES = ` Team@Test.local : staff , dev ; ${OPS_EMAIL}: ops `;
160
+
161
+ await invokeDevLogin({ email: 'team@test.local' });
162
+
163
+ expect((await userRow(TEAM_EMAIL))!.roles).toEqual(['staff', 'dev']);
164
+ expect(await auditRows()).toHaveLength(2);
165
+ });
166
+
167
+ it('a role the catalog does not know is refused and named; the known roles in the same entry are still granted', async () => {
168
+ process.env.DEV_BOOTSTRAP_ROLES = `${TEAM_EMAIL}:staff,no-such-role`;
169
+
170
+ await invokeDevLogin({ email: TEAM_EMAIL });
171
+
172
+ expect((await userRow(TEAM_EMAIL))!.roles).toEqual(['staff']);
173
+ expect(await auditRows()).toHaveLength(1);
174
+ expect(markerLines()).toEqual([
175
+ `[dev-bootstrap] ${TEAM_EMAIL}: granted staff; held (none); refused no-such-role (unknown role)`,
176
+ ]);
177
+ });
178
+
179
+ it("break-glass is never this door's to grant: 'admin' in the list is refused and named (the first-admin door is the one path)", async () => {
180
+ process.env.DEV_BOOTSTRAP_ROLES = `${TEAM_EMAIL}:admin,dev`;
181
+
182
+ await invokeDevLogin({ email: TEAM_EMAIL });
183
+
184
+ expect((await userRow(TEAM_EMAIL))!.roles).toEqual(['dev']);
185
+ expect(await adminRows()).toHaveLength(0);
186
+ expect((await auditRows()).map((e) => e.role)).toEqual(['dev']);
187
+ expect(markerLines()).toEqual([
188
+ `[dev-bootstrap] ${TEAM_EMAIL}: granted dev; held (none); refused admin (break-glass)`,
189
+ ]);
190
+ });
191
+
192
+ it('the default path is a request for DEV_AUTO_LOGIN_EMAIL: when that address is listed, the default account carries the roles', async () => {
193
+ process.env.DEV_BOOTSTRAP_ROLES = `${ENV_EMAIL}:dev`;
194
+
195
+ const outcome = await invokeDevLogin();
196
+
197
+ expect(outcome.loggedInAs).toBe(ENV_EMAIL);
198
+ expect((await userRow(ENV_EMAIL))!.roles).toEqual(['dev']);
199
+ expect(await auditRows()).toHaveLength(1);
200
+ });
201
+
202
+ it('composes with the first-admin door: the same address listed in both gets admin first, then its roles — three audited grants, one account', async () => {
203
+ process.env.DEV_BOOTSTRAP_ADMIN_EMAIL = TEAM_EMAIL;
204
+
205
+ await invokeDevLogin({ email: TEAM_EMAIL });
206
+
207
+ expect((await userRow(TEAM_EMAIL))!.roles).toEqual(['admin', 'staff', 'dev']);
208
+ expect(await auditRows()).toHaveLength(3);
209
+ expect(await adminRows()).toHaveLength(1);
210
+ });
211
+
212
+ it('with the variable unset a listed address is an ordinary account — the omission is the safety (a deployment outside development never sets it)', async () => {
213
+ delete process.env.DEV_BOOTSTRAP_ROLES;
214
+
215
+ await invokeDevLogin({ email: TEAM_EMAIL });
216
+
217
+ expect((await userRow(TEAM_EMAIL))!.roles).toEqual([]);
218
+ expect(await auditRows()).toHaveLength(0);
219
+ expect(markerLines()).toEqual([]);
220
+ });
221
+
222
+ it('malformed entries (no colon, no roles) are ignored; the well-formed entries beside them are honored', async () => {
223
+ process.env.DEV_BOOTSTRAP_ROLES = `garbage;${OPS_EMAIL}:;${TEAM_EMAIL}:dev;:ops`;
224
+
225
+ await invokeDevLogin({ email: TEAM_EMAIL });
226
+ await invokeDevLogin({ email: OPS_EMAIL });
227
+
228
+ expect((await userRow(TEAM_EMAIL))!.roles).toEqual(['dev']);
229
+ expect((await userRow(OPS_EMAIL))!.roles).toEqual([]);
230
+ expect(await auditRows()).toHaveLength(1);
231
+ });
232
+
233
+ it.each([['DEVELOPMENT'], ['DEV_AUTO_LOGIN_EMAIL']])(
234
+ 'gate closed (%s unset): 404 exactly as before — no session, no account, no grant; the variable changes nothing',
235
+ async (gate) => {
236
+ delete process.env[gate];
237
+
238
+ const outcome = await invokeDevLogin({ email: TEAM_EMAIL });
239
+
240
+ expect(outcome.status).toBe(404);
241
+ expect(outcome.loggedInAs).toBeUndefined();
242
+ expect(outcome.sessionSaved).toBe(false);
243
+ expect(await userRow(TEAM_EMAIL)).toBeUndefined();
244
+ expect(await auditRows()).toHaveLength(0);
245
+ expect(markerLines()).toEqual([]);
246
+ }
247
+ );
248
+ });
@@ -9,8 +9,8 @@ const activityRows = async (scope: string): Promise<UserActivity[]> =>
9
9
  await getDbAsSystem().query(tables.UserActivity, { scope });
10
10
 
11
11
  /**
12
- * TRANSPORT IS NOT PRESENCE (UserActivityTable's contract; founder finding 2026-09-12 — every
13
- * user on the admin usage page "last active today"). The per-request session-cache build
12
+ * TRANSPORT IS NOT PRESENCE (UserActivityTable's contract; the defect it names: every user read
13
+ * as "last active today"). The per-request session-cache build
14
14
  * (`userCache.create`) runs for EVERY request that rides a session cookie: an open tab's polls,
15
15
  * a socket's room re-joins on every reconnect, the reload a deploy pushes onto every idle tab.
16
16
  * None of that is a person. The build must therefore never write the presence stamp — the