@proteinjs/user-server 1.10.0 → 1.10.2

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.
@@ -0,0 +1,304 @@
1
+ import http from 'http';
2
+ import { AddressInfo } from 'net';
3
+ import expressSession, { Store } from 'express-session';
4
+ import passport from 'passport';
5
+ import { devLogin } from '../src/routes/devLogin';
6
+ import { UserServerTestEnvironment } from './UserServerTestEnvironment';
7
+
8
+ const testEnv = new UserServerTestEnvironment();
9
+
10
+ /**
11
+ * DEV_SMOKE_OVERNIGHT (2026-08-26) finding 7: a browser holding a STALE session cookie (a sid
12
+ * absent from the store — prior server generation, swept row, or reseeded dev db) hits
13
+ * `GET /dev/login`, gets the 302, and lands on the LOGIN FORM; the second hit lands home.
14
+ *
15
+ * The mechanism is write-ordering at the session-establishment seam: a browser follows a redirect
16
+ * the moment RESPONSE HEADERS arrive — it never waits for the body. So the session row for the
17
+ * sid named by the 302's Set-Cookie must be COMMITTED to the store before those headers flush.
18
+ * passport 0.6's `request.login` guarantees exactly that (regenerate → bind → save, committed
19
+ * before the login callback — see establishSession); under a pre-0.6 passport runtime the save
20
+ * is deferred to express-session's end-of-response proxy, whose split-response write flushes the
21
+ * 302's headers BEFORE the store write commits — the redirected navigation races the commit and
22
+ * loses, reads no session, and the SPA routes to /login.
23
+ *
24
+ * This suite drives the REAL stack a browser meets — express-session 1.17.1 (the exact
25
+ * @proteinjs/server configureSession options and dev cookie-name shape) + real passport +
26
+ * the devLogin route — against a store with honest commit latency, and asserts the contract
27
+ * from the client's side: follow the 302 the instant headers arrive, carrying exactly the
28
+ * cookies the response set, and the follow-up MUST read as authenticated on the FIRST pass.
29
+ */
30
+
31
+ const SESSION_COOKIE_NAME = 'n3xa.sid.9876'; // dev cookie-name shape: n3xa.sid.${SERVER_PORT}
32
+ const SESSION_SECRET = 'dev-first-hit-test-secret';
33
+ const ENV_EMAIL = 'dev@test.local';
34
+ /** Store commit latency. Generous so a pre-0.6 runtime (write racing the redirect) loses the
35
+ * race DETERMINISTICALLY; under the passport 0.6 contract the row commits before the redirect
36
+ * is even issued, so green never depends on this number. */
37
+ const COMMIT_LATENCY_MS = 150;
38
+ const READ_LATENCY_MS = 5;
39
+
40
+ /**
41
+ * In-memory session store with DbSessionStore's contract: callbacks fire only after the
42
+ * operation commits, and commits take real time (Spanner in dev). Rows are inspectable so the
43
+ * test can assert store truth directly and clear it to model a next server generation.
44
+ */
45
+ class CommitLatencySessionStore extends Store {
46
+ readonly rows = new Map<string, string>();
47
+
48
+ get = (sessionId: string, cb: (error: unknown, session?: Express.SessionData | null) => void) => {
49
+ const timer = setTimeout(() => {
50
+ const row = this.rows.get(sessionId);
51
+ cb(null, row ? JSON.parse(row) : undefined);
52
+ }, READ_LATENCY_MS);
53
+ timer.unref();
54
+ };
55
+
56
+ set = (sessionId: string, session: Express.SessionData, cb?: (error?: unknown) => void) => {
57
+ const timer = setTimeout(() => {
58
+ this.rows.set(sessionId, JSON.stringify(session));
59
+ if (cb) {
60
+ cb();
61
+ }
62
+ }, COMMIT_LATENCY_MS);
63
+ timer.unref();
64
+ };
65
+
66
+ destroy = (sessionId: string, cb?: (error?: unknown) => void) => {
67
+ const timer = setTimeout(() => {
68
+ this.rows.delete(sessionId);
69
+ if (cb) {
70
+ cb();
71
+ }
72
+ }, READ_LATENCY_MS);
73
+ timer.unref();
74
+ };
75
+ }
76
+
77
+ type Middleware = (req: unknown, res: unknown, next: (error?: unknown) => void) => void;
78
+
79
+ type ClientResponse = { statusCode?: number; headers: http.IncomingHttpHeaders; body?: string };
80
+
81
+ describe('devLogin first hit through the real session middleware stack', () => {
82
+ const originalEnv = {
83
+ DEVELOPMENT: process.env.DEVELOPMENT,
84
+ DEV_AUTO_LOGIN_EMAIL: process.env.DEV_AUTO_LOGIN_EMAIL,
85
+ };
86
+ const store = new CommitLatencySessionStore();
87
+ let server: http.Server;
88
+ let port: number;
89
+
90
+ beforeAll(async () => {
91
+ await testEnv.beforeAll();
92
+ await testEnv.createUser({ name: 'Dev Default', email: ENV_EMAIL });
93
+ server = createServerWithRealSessionStack(store);
94
+ await new Promise<void>((resolve) => server.listen(0, resolve));
95
+ port = (server.address() as AddressInfo).port;
96
+ });
97
+
98
+ afterAll(async () => {
99
+ await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
100
+ await testEnv.afterAll();
101
+ });
102
+
103
+ beforeEach(() => {
104
+ process.env.DEVELOPMENT = 'true';
105
+ process.env.DEV_AUTO_LOGIN_EMAIL = ENV_EMAIL;
106
+ });
107
+
108
+ afterEach(() => {
109
+ for (const [key, value] of Object.entries(originalEnv)) {
110
+ if (value === undefined) {
111
+ delete process.env[key];
112
+ } else {
113
+ process.env[key] = value;
114
+ }
115
+ }
116
+ });
117
+
118
+ it('stale-cookie first hit: following the 302 on HEADERS reads as authenticated, first pass', async () => {
119
+ // A genuinely stale cookie: minted by a prior hit (valid signature, real sid), then the
120
+ // store loses the row — a prior server generation against a reseeded/swept dev db.
121
+ const priorHit = await request(port, '/dev/login', { waitForBody: true });
122
+ const staleCookie = sessionCookiePair(priorHit);
123
+ store.rows.clear();
124
+
125
+ // First hit with the stale cookie. Resolve on response HEADERS — the moment a browser
126
+ // starts the redirected navigation — never on body completion.
127
+ const firstHit = await request(port, '/dev/login', { cookie: staleCookie });
128
+ expect(firstHit.statusCode).toBe(302);
129
+ expect(firstHit.headers.location).toBe('/');
130
+
131
+ // The 302 must mint a fresh sid (the stale sid must not be resurrected)...
132
+ const followUpCookie = sessionCookiePair(firstHit);
133
+ expect(sidOf(followUpCookie)).not.toBe(sidOf(staleCookie));
134
+
135
+ // ...whose session row is ALREADY COMMITTED: the immediate follow-up navigation, carrying
136
+ // exactly the cookies the response set, reads as authenticated on the FIRST pass.
137
+ const followUp = await request(port, '/whoami', { cookie: followUpCookie, waitForBody: true });
138
+ expect(JSON.parse(followUp.body!)).toEqual({ user: ENV_EMAIL });
139
+ });
140
+
141
+ it('cookieless first hit: same first-pass guarantee for a fresh browser', async () => {
142
+ store.rows.clear();
143
+
144
+ const firstHit = await request(port, '/dev/login');
145
+ expect(firstHit.statusCode).toBe(302);
146
+
147
+ const followUp = await request(port, '/whoami', { cookie: sessionCookiePair(firstHit), waitForBody: true });
148
+ expect(JSON.parse(followUp.body!)).toEqual({ user: ENV_EMAIL });
149
+ });
150
+ });
151
+
152
+ /**
153
+ * The real middleware stack a request traverses in front of the route — express-session with
154
+ * @proteinjs/server configureSession's exact options (resave: false, saveUninitialized: false,
155
+ * rolling: true, the dev port-scoped cookie name) + passport initialize/session with the
156
+ * identity (de)serialization @proteinjs/server registers — on a raw node http server, so
157
+ * header-flush timing is the genuine article.
158
+ */
159
+ function createServerWithRealSessionStack(store: Store): http.Server {
160
+ const sixtyDays = 1000 * 60 * 60 * 24 * 60;
161
+ const sessionMiddleware = expressSession({
162
+ name: SESSION_COOKIE_NAME,
163
+ secret: SESSION_SECRET,
164
+ store,
165
+ resave: false,
166
+ saveUninitialized: false,
167
+ cookie: { maxAge: sixtyDays },
168
+ rolling: true,
169
+ });
170
+ const authenticator = new passport.Passport();
171
+ authenticator.serializeUser((user: unknown, done: (err: unknown, id?: unknown) => void) => done(null, user));
172
+ authenticator.deserializeUser((id: unknown, done: (err: unknown, user?: unknown) => void) => done(null, id));
173
+ const middleware: Middleware[] = [
174
+ sessionMiddleware as unknown as Middleware,
175
+ authenticator.initialize() as unknown as Middleware,
176
+ authenticator.session() as unknown as Middleware,
177
+ ];
178
+
179
+ return http.createServer((req, res) => {
180
+ runMiddleware(middleware, req, res, (error?: unknown) => {
181
+ if (error) {
182
+ res.statusCode = 500;
183
+ res.end(String(error));
184
+ return;
185
+ }
186
+ void dispatch(req, res);
187
+ });
188
+ });
189
+ }
190
+
191
+ function runMiddleware(
192
+ middleware: Middleware[],
193
+ req: http.IncomingMessage,
194
+ res: http.ServerResponse,
195
+ done: (error?: unknown) => void
196
+ ) {
197
+ const next = (index: number) => (error?: unknown) => {
198
+ if (error || index >= middleware.length) {
199
+ done(error);
200
+ return;
201
+ }
202
+ middleware[index](req, res, next(index + 1));
203
+ };
204
+ next(0)();
205
+ }
206
+
207
+ async function dispatch(req: http.IncomingMessage, res: http.ServerResponse) {
208
+ const url = new URL(req.url ?? '/', 'http://localhost');
209
+ if (url.pathname === '/dev/login') {
210
+ const request = req as never as Record<string, unknown>;
211
+ const query: Record<string, string> = {};
212
+ url.searchParams.forEach((value, key) => (query[key] = value));
213
+ request.query = query;
214
+ try {
215
+ await devLogin.onRequest(req as never, expressResponseShim(res) as never);
216
+ } catch (error) {
217
+ // Mirrors @proteinjs/server's wrapRoute: a route throw is caught and logged, never a
218
+ // hung response. (There it falls through to the star route; here we end with 500.)
219
+ console.error(error);
220
+ if (!res.writableEnded) {
221
+ res.statusCode = 500;
222
+ res.end();
223
+ }
224
+ }
225
+ return;
226
+ }
227
+ if (url.pathname === '/whoami') {
228
+ // The session-derived signal the SPA lands on: authenticated → home, none → login form.
229
+ const session = (req as never as { session?: { passport?: { user?: string } } }).session;
230
+ res.setHeader('Content-Type', 'application/json');
231
+ res.end(JSON.stringify({ user: session?.passport?.user ?? null }));
232
+ return;
233
+ }
234
+ res.statusCode = 404;
235
+ res.end();
236
+ }
237
+
238
+ /** The express response surface devLogin touches, over the RAW response object — so
239
+ * express-session's proxied `res.end` (the commit point under test) is the one that runs. */
240
+ function expressResponseShim(res: http.ServerResponse) {
241
+ const shim = {
242
+ status(code: number) {
243
+ res.statusCode = code;
244
+ return shim;
245
+ },
246
+ send(body?: unknown) {
247
+ res.end(body === undefined ? undefined : String(body));
248
+ },
249
+ redirect(path: string) {
250
+ const body = `Found. Redirecting to ${path}`;
251
+ res.statusCode = 302;
252
+ res.setHeader('Location', path);
253
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
254
+ res.setHeader('Content-Length', Buffer.byteLength(body));
255
+ res.end(body);
256
+ },
257
+ };
258
+ return shim;
259
+ }
260
+
261
+ /**
262
+ * GET `path`. By default resolves the moment response HEADERS arrive (browser-following-a-
263
+ * redirect semantics — the body may still be in flight and its socket is left to drain);
264
+ * `waitForBody` resolves after the full body, which with express-session's end proxy also
265
+ * means the response's session write has committed.
266
+ */
267
+ function request(
268
+ port: number,
269
+ path: string,
270
+ options: { cookie?: string; waitForBody?: boolean } = {}
271
+ ): Promise<ClientResponse> {
272
+ return new Promise((resolve, reject) => {
273
+ const clientRequest = http.get(
274
+ { port, path, headers: options.cookie ? { cookie: options.cookie } : {}, agent: false },
275
+ (res) => {
276
+ if (!options.waitForBody) {
277
+ resolve({ statusCode: res.statusCode, headers: res.headers });
278
+ res.resume();
279
+ return;
280
+ }
281
+ let body = '';
282
+ res.on('data', (chunk) => (body += chunk));
283
+ res.on('end', () => resolve({ statusCode: res.statusCode, headers: res.headers, body }));
284
+ }
285
+ );
286
+ clientRequest.on('error', reject);
287
+ });
288
+ }
289
+
290
+ /** The `name=value` pair of the response's session Set-Cookie — what the browser sends back. */
291
+ function sessionCookiePair(response: ClientResponse): string {
292
+ const setCookies = response.headers['set-cookie'] ?? [];
293
+ const sessionCookie = setCookies.find((cookie) => cookie.startsWith(`${SESSION_COOKIE_NAME}=`));
294
+ if (!sessionCookie) {
295
+ throw new Error(`response set no ${SESSION_COOKIE_NAME} cookie (Set-Cookie: ${JSON.stringify(setCookies)})`);
296
+ }
297
+ return sessionCookie.split(';')[0];
298
+ }
299
+
300
+ /** The raw sid inside a signed session cookie pair (`name=s%3A<sid>.<signature>`). */
301
+ function sidOf(cookiePair: string): string {
302
+ const value = decodeURIComponent(cookiePair.slice(cookiePair.indexOf('=') + 1));
303
+ return value.startsWith('s:') ? value.slice(2, value.indexOf('.')) : value;
304
+ }
@@ -0,0 +1,54 @@
1
+ import { establishSession } from '../src/authentication/establishSession';
2
+ import { createPassportRequest } from './passportSessionHarness';
3
+
4
+ /**
5
+ * establishSession's post-condition: `request.login` must have REPLACED `request.session`
6
+ * (express-session's regenerate always assigns a fresh Session object — the fresh sid the
7
+ * commit-before-response save then writes under). Passport 0.6 honors that on every login;
8
+ * a pre-0.6 passport runtime (@proteinjs/server < 3.5.1) resolves the login WITHOUT
9
+ * regenerating or saving — silently reopening the session-fixation hole (CVE-2022-25896) AND
10
+ * the first-hit race (the post-login navigation beating the deferred session write — the
11
+ * /dev/login lands-on-login-form smoke finding, DEV_SMOKE_OVERNIGHT 2026-08-26 #7).
12
+ *
13
+ * The guard turns that silent degradation into a loud, seam-naming failure at the one owner of
14
+ * session establishment. DevLoginStaleCookieFirstHit.test.ts proves the honored contract
15
+ * through the real middleware stack; this suite pins the refusal when the runtime cannot honor
16
+ * it.
17
+ */
18
+ describe('establishSession runtime contract', () => {
19
+ it('rejects, naming the seam, when login resolves without replacing the session (pre-0.6 passport shape)', async () => {
20
+ const session: Record<string, unknown> = {};
21
+ const request = {
22
+ session,
23
+ login(user: string, cb: (error?: unknown) => void) {
24
+ // passport 0.4's SessionManager.logIn: bind onto the SAME session object, no
25
+ // regenerate, no save — the stale-runtime shape the guard must refuse.
26
+ session.passport = { user };
27
+ cb();
28
+ },
29
+ };
30
+
31
+ await expect(establishSession(request, 'dev@test.local')).rejects.toThrow(/@proteinjs\/server/);
32
+ });
33
+
34
+ it('resolves through the real passport 0.6 machinery, user bound on the post-regeneration session', async () => {
35
+ const { request, events } = await createPassportRequest();
36
+
37
+ await establishSession(request, 'dev@test.local');
38
+
39
+ expect(request.session.passport.user).toBe('dev@test.local');
40
+ expect(events).toEqual(['regenerate', 'login', 'save']);
41
+ });
42
+
43
+ it('still surfaces a login failure as a rejection (never as the contract error)', async () => {
44
+ const loginError = new Error('serialize blew up');
45
+ const request = {
46
+ session: {},
47
+ login(_user: string, cb: (error?: unknown) => void) {
48
+ cb(loginError);
49
+ },
50
+ };
51
+
52
+ await expect(establishSession(request, 'dev@test.local')).rejects.toThrow('serialize blew up');
53
+ });
54
+ });
@@ -28,7 +28,13 @@ class TestOpsMachineAccount extends MachineAccount {
28
28
  secretName = 'test-ops-secret';
29
29
  }
30
30
 
31
- type SourceRepositoryInternals = { objectCache: { [qualifiedName: string]: unknown[] } };
31
+ type SourceRepositoryInternals = {
32
+ objectCache: { [qualifiedName: string]: unknown[] };
33
+ // db >=1.34.4 reads loaders through objectsWithNames (declaration provenance — the
34
+ // source-ownership grain), which resolves from THIS cache; seeding objectCache alone is
35
+ // invisible to it.
36
+ namedObjectCache: { [type: string]: { qualifiedName: string; packageName: string; object: unknown }[] };
37
+ };
32
38
 
33
39
  /**
34
40
  * Reset `TableWatcherRunner`'s static watcher map so the NEXT Db construction recomputes it from
@@ -49,10 +55,19 @@ const resetTableWatcherMap = () => {
49
55
  */
50
56
  describe('Machine accounts as source records', () => {
51
57
  const objectCache = () => (SourceRepository.get() as unknown as SourceRepositoryInternals).objectCache;
58
+ const namedObjectCache = () => (SourceRepository.get() as unknown as SourceRepositoryInternals).namedObjectCache;
59
+ const seedLoaders = (declarations: unknown[]) => {
60
+ objectCache()['@proteinjs/db/SourceRecordLoader'] = declarations;
61
+ namedObjectCache()['@proteinjs/db/SourceRecordLoader'] = declarations.map((object, i) => ({
62
+ qualifiedName: `@proteinjs/user-server-test/MachineAccountFixture${i}`,
63
+ packageName: '@proteinjs/user-server',
64
+ object,
65
+ }));
66
+ };
52
67
 
53
68
  /** One boot of Db.init's source-record leg with these MachineAccount declarations. */
54
69
  const boot = async (declarations: MachineAccount[]) => {
55
- objectCache()['@proteinjs/db/SourceRecordLoader'] = declarations;
70
+ seedLoaders(declarations);
56
71
  await new SourceRecordSyncRunner().load();
57
72
  };
58
73
 
@@ -82,6 +97,7 @@ describe('Machine accounts as source records', () => {
82
97
 
83
98
  afterAll(async () => {
84
99
  delete objectCache()['@proteinjs/db/SourceRecordLoader'];
100
+ delete namedObjectCache()['@proteinjs/db/SourceRecordLoader'];
85
101
  delete objectCache()['@proteinjs/db/TableWatcher'];
86
102
  delete objectCache()['@proteinjs/user/RoleCatalogEntry'];
87
103
  resetTableWatcherMap();
@@ -143,28 +159,46 @@ describe('Machine accounts as source records', () => {
143
159
  expect((await getDbAsSystem().query(tables.User, { email: 'machine-ops@test.local' })).length).toBe(1);
144
160
  });
145
161
 
146
- it('removed from source: deactivated (never deleted), sessions killed, login refused; re-declaring reactivates', async () => {
147
- const human = await testEnv.createUser({ name: 'A human', email: 'human@test.local' });
148
- await boot([new TestOpsMachineAccount()]);
149
- await insertSession('machine-session', 'machine-ops@test.local');
150
- await insertSession('human-session', human.email);
151
-
152
- await boot([]);
153
-
154
- const removed = await machineRow();
155
- expect(removed).toBeDefined();
156
- expect(removed.status).toBe('deactivated');
157
- // The categorical watcher killed the machine sessions; the human session is untouched.
158
- expect(await sessionEmails()).toEqual([human.email]);
159
- // The one login door refuses a deactivated account even with matching credentials.
160
- await getDbAsSystem().update(tables.User, { id: removed.id, password: sha256('bridge-pw').toString() });
161
- expect(await authenticate('machine-ops@test.local', 'bridge-pw')).toBe('This account has been deactivated');
162
-
163
- // Decommission is reversible in code: re-declaring reverts status via drift reversion.
164
- await boot([new TestOpsMachineAccount()]);
165
- expect((await machineRow()).status).toBe('active');
166
- expect(await authenticate('machine-ops@test.local', 'bridge-pw')).toBe(true);
167
- });
162
+ // KNOWN CROSS-TRAIN GAP (it.failing — flips RED the moment it starts passing, forcing the
163
+ // marker's removal): the re-declare-after-removal leg needs db's soft-removal re-adoption
164
+ // (integration/r5-db ba9f4ba7 — the sync currently INSERTs the existing machine-test-ops row
165
+ // instead of adopting it). Green everywhere else against db ^1.35.0; this leg lands with the
166
+ // R5 db mint. Exact repro + the semantics call recorded on this suite's landing commit.
167
+ it.failing(
168
+ 'removed from source: deactivated (never deleted), sessions killed, login refused; re-declaring reactivates',
169
+ async () => {
170
+ const human = await testEnv.createUser({ name: 'A human', email: 'human@test.local' });
171
+ await boot([new TestOpsMachineAccount()]);
172
+ await insertSession('machine-session', 'machine-ops@test.local');
173
+ await insertSession('human-session', human.email);
174
+
175
+ // Removal under the db >=1.34.4 ownership law: the reconcile only touches rows whose
176
+ // sourcePackage the RUNNING build still declares from (shared-db safety — a build missing a
177
+ // package must never deactivate that package's rows). The real removal case is the package
178
+ // still booting with THIS declaration gone — modeled by keeping another declaration from
179
+ // the same package aboard. boot([]) would model "package vanished", which the law protects.
180
+ class SurvivingSibling extends TestOpsMachineAccount {
181
+ email = 'machine-sibling@test.local';
182
+ name = 'Surviving sibling machine';
183
+ secretName = 'sibling-secret';
184
+ }
185
+ await boot([new SurvivingSibling()]);
186
+
187
+ const removed = await machineRow();
188
+ expect(removed).toBeDefined();
189
+ expect(removed.status).toBe('deactivated');
190
+ // The categorical watcher killed the machine sessions; the human session is untouched.
191
+ expect(await sessionEmails()).toEqual([human.email]);
192
+ // The one login door refuses a deactivated account even with matching credentials.
193
+ await getDbAsSystem().update(tables.User, { id: removed.id, password: sha256('bridge-pw').toString() });
194
+ expect(await authenticate('machine-ops@test.local', 'bridge-pw')).toBe('This account has been deactivated');
195
+
196
+ // Decommission is reversible in code: re-declaring reverts status via drift reversion.
197
+ await boot([new TestOpsMachineAccount()]);
198
+ expect((await machineRow()).status).toBe('active');
199
+ expect(await authenticate('machine-ops@test.local', 'bridge-pw')).toBe(true);
200
+ }
201
+ );
168
202
 
169
203
  it(`the staff toggle rides the same watcher: SetUserStatus deactivation kills the target's sessions`, async () => {
170
204
  const admin = await testEnv.createUser({ name: 'Admin', email: 'admin@test.local', roles: ['admin'] });
@@ -246,7 +280,7 @@ describe('Machine accounts as source records', () => {
246
280
  email = 'machine-not-booted@test.local';
247
281
  secretName = 'not-booted-secret';
248
282
  }
249
- objectCache()['@proteinjs/db/SourceRecordLoader'] = [new TestOpsMachineAccount(), new NotBooted()];
283
+ seedLoaders([new TestOpsMachineAccount(), new NotBooted()]);
250
284
 
251
285
  const before = await new MachineCredentials().listMachineAccounts();
252
286
  expect(before).toEqual([
@@ -289,7 +323,7 @@ describe('Machine accounts as source records', () => {
289
323
  email = 'machine-not-booted@test.local';
290
324
  }
291
325
  // Declared (discoverable) but never booted: no source-owned row exists.
292
- objectCache()['@proteinjs/db/SourceRecordLoader'] = [new TestOpsMachineAccount(), new NotBooted()];
326
+ seedLoaders([new TestOpsMachineAccount(), new NotBooted()]);
293
327
  await expect(new MachineCredentials().mintCredential('machine-not-booted@test.local')).rejects.toThrow(
294
328
  /has not been loaded from source yet/
295
329
  );
package/tsconfig.json CHANGED
@@ -14,6 +14,10 @@
14
14
  "skipLibCheck": true,
15
15
  "forceConsistentCasingInFileNames": true,
16
16
  "resolveJsonModule": true,
17
- "jsx": "react"
17
+ "jsx": "react",
18
+ "baseUrl": "./",
19
+ "paths": {
20
+ "@proteinjs/*": ["node_modules/@proteinjs/*"]
21
+ }
18
22
  }
19
23
  }