@remit/auth-service 0.0.15 → 0.1.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/src/people.ts ADDED
@@ -0,0 +1,541 @@
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
2
+ import type {
3
+ Person,
4
+ PersonStatus,
5
+ StepUpAction,
6
+ } from "@remit/api-openapi-types";
7
+ import { deriveAccountConfigId } from "@remit/data-ports/id";
8
+ import { calendarFeedTokens } from "@remit/drizzle-sqlite-schema";
9
+ import { and, asc, count, eq, gt, lte, sql } from "drizzle-orm";
10
+ import type { Auth } from "./auth.js";
11
+ import {
12
+ applySignInSwitch,
13
+ findUser,
14
+ HOST_CLIENT_IP,
15
+ type IdentityStore,
16
+ revokeUser,
17
+ type UserRow,
18
+ } from "./identity-store.js";
19
+ import {
20
+ auth_account,
21
+ auth_session,
22
+ auth_user,
23
+ auth_verification,
24
+ identity_audit_event,
25
+ identity_step_up,
26
+ } from "./schema/auth-schema-sqlite.js";
27
+
28
+ export type { Person, PersonStatus, StepUpAction };
29
+
30
+ export interface StepUpGrant {
31
+ stepUpToken: string;
32
+ expiresAt: number;
33
+ }
34
+
35
+ export interface PasswordReset {
36
+ person: Person;
37
+ resetUrl: string;
38
+ expiresAt: number;
39
+ }
40
+
41
+ export type PeopleRefusal =
42
+ | "caller_disabled"
43
+ | "not_found"
44
+ | "step_up_required"
45
+ | "wrong_password"
46
+ | "step_up_locked"
47
+ | "cannot_disable_self";
48
+
49
+ export interface Refused {
50
+ ok: false;
51
+ refusal: PeopleRefusal;
52
+ }
53
+
54
+ export type PeopleOutcome<T> = { ok: true; value: T } | Refused;
55
+
56
+ export interface PeopleRequest {
57
+ callerId: string;
58
+ clientIp: string;
59
+ }
60
+
61
+ export interface TargetedRequest extends PeopleRequest {
62
+ targetId: string;
63
+ stepUpToken: string;
64
+ }
65
+
66
+ export interface StepUpRequest extends PeopleRequest {
67
+ targetId: string;
68
+ action: StepUpAction;
69
+ password: string;
70
+ }
71
+
72
+ export type GuardRefusal = "RefusedSignInOff" | "RefusedForeignOrigin";
73
+
74
+ export interface PeopleDirectory {
75
+ listPeople(request: PeopleRequest): Promise<PeopleOutcome<Person[]>>;
76
+ createStepUp(request: StepUpRequest): Promise<PeopleOutcome<StepUpGrant>>;
77
+ signOutPerson(request: TargetedRequest): Promise<PeopleOutcome<Person>>;
78
+ updatePerson(
79
+ request: TargetedRequest & { status: PersonStatus },
80
+ ): Promise<PeopleOutcome<Person>>;
81
+ createPasswordReset(
82
+ request: TargetedRequest,
83
+ ): Promise<PeopleOutcome<PasswordReset>>;
84
+ recordRefusal(
85
+ request: PeopleRequest & { targetId: string; outcome: GuardRefusal },
86
+ ): void;
87
+ }
88
+
89
+ export interface PeopleAdmin extends PeopleDirectory {
90
+ enableByEmail(email: string): Promise<PeopleOutcome<Person>>;
91
+ resetByEmail(email: string): Promise<PeopleOutcome<PasswordReset>>;
92
+ applySignInSwitch(signInEnabled: boolean): void;
93
+ close(): void;
94
+ }
95
+
96
+ export interface PeopleConfig {
97
+ auth: Auth;
98
+ store: IdentityStore;
99
+ baseURL: string;
100
+ }
101
+
102
+ const STEP_UP_TTL_MS = 5 * 60 * 1000;
103
+ const STEP_UP_WINDOW_MS = 15 * 60 * 1000;
104
+ const STEP_UP_MAX_FAILURES = 5;
105
+ const RESET_TTL_MS = 24 * 60 * 60 * 1000;
106
+ const CREDENTIAL_PROVIDER = "credential";
107
+ const SHELL_ACTOR = "shell";
108
+
109
+ const refused = (refusal: PeopleRefusal): Refused => ({ ok: false, refusal });
110
+
111
+ const ok = <T>(value: T): { ok: true; value: T } => ({ ok: true, value });
112
+
113
+ const newSecret = (): string => randomBytes(32).toString("base64url");
114
+
115
+ const digestOf = (value: string): string =>
116
+ createHash("sha256").update(value).digest("base64url");
117
+
118
+ /**
119
+ * The identifier better-auth looks a reset row up by under
120
+ * `verification.storeIdentifier: { overrides: { "reset-password": "hashed" } }`:
121
+ * the SHA-256 of `reset-password:<token>`, base64url without padding.
122
+ */
123
+ export const resetIdentifierOf = (token: string): string =>
124
+ digestOf(`reset-password:${token}`);
125
+
126
+ const activeSessionCounts = (store: IdentityStore): Map<string, number> =>
127
+ new Map(
128
+ store.db
129
+ .select({ userId: auth_session.userId, sessions: count() })
130
+ .from(auth_session)
131
+ .where(gt(auth_session.expiresAt, new Date()))
132
+ .groupBy(auth_session.userId)
133
+ .all()
134
+ .map((row) => [row.userId, row.sessions]),
135
+ );
136
+
137
+ const toPerson = (
138
+ user: UserRow,
139
+ callerId: string,
140
+ sessions: Map<string, number>,
141
+ ): Person => ({
142
+ userId: user.userId,
143
+ email: user.email,
144
+ createdAt: user.createdAt.getTime(),
145
+ lastSignInAt: user.lastSignInAt.getTime(),
146
+ sessionCount: sessions.get(user.userId) ?? 0,
147
+ status: user.disabled ? "Disabled" : "Active",
148
+ isCaller: user.userId === callerId,
149
+ });
150
+
151
+ const personOf = (
152
+ store: IdentityStore,
153
+ userId: string,
154
+ callerId: string,
155
+ ): Person => {
156
+ const user = findUser(store, userId);
157
+ if (!user)
158
+ throw new Error(`user ${userId} vanished inside its own transaction`);
159
+ return toPerson(user, callerId, activeSessionCounts(store));
160
+ };
161
+
162
+ const userByEmail = (
163
+ store: IdentityStore,
164
+ email: string,
165
+ ): UserRow | undefined => {
166
+ const row = store.db
167
+ .select({ userId: auth_user.id })
168
+ .from(auth_user)
169
+ .where(eq(sql`lower(${auth_user.email})`, email.trim().toLowerCase()))
170
+ .get();
171
+ return row && findUser(store, row.userId);
172
+ };
173
+
174
+ /**
175
+ * Whether the caller may act at all, read inside the write transaction so two
176
+ * users disabling each other are serialized and the second finds itself
177
+ * disabled. A refusal is audited in the same transaction, which then commits.
178
+ */
179
+ const callerRefusal = (
180
+ store: IdentityStore,
181
+ request: PeopleRequest,
182
+ target: string,
183
+ ): Refused | undefined => {
184
+ const caller = findUser(store, request.callerId);
185
+ if (caller && !caller.disabled) return undefined;
186
+ store.audit({
187
+ actor: request.callerId,
188
+ target,
189
+ action: "PeopleRequestRefused",
190
+ outcome: "RefusedCallerDisabled",
191
+ clientIp: request.clientIp,
192
+ });
193
+ return refused("caller_disabled");
194
+ };
195
+
196
+ const stepUpRefusal = (
197
+ store: IdentityStore,
198
+ request: TargetedRequest,
199
+ action: StepUpAction,
200
+ ): Refused | undefined => {
201
+ const spent = store.db
202
+ .delete(identity_step_up)
203
+ .where(
204
+ and(
205
+ eq(identity_step_up.digest, digestOf(request.stepUpToken)),
206
+ eq(identity_step_up.callerUserId, request.callerId),
207
+ eq(identity_step_up.action, action),
208
+ eq(identity_step_up.targetUserId, request.targetId),
209
+ gt(identity_step_up.expiresAt, new Date()),
210
+ ),
211
+ )
212
+ .run();
213
+ if (spent.changes === 1) return undefined;
214
+ store.audit({
215
+ actor: request.callerId,
216
+ target: request.targetId,
217
+ action: "PeopleRequestRefused",
218
+ outcome: "RefusedStepUp",
219
+ clientIp: request.clientIp,
220
+ });
221
+ return refused("step_up_required");
222
+ };
223
+
224
+ const issueReset = (
225
+ store: IdentityStore,
226
+ userId: string,
227
+ scrambledHash: string,
228
+ ): { token: string; expiresAt: Date } => {
229
+ const { db } = store;
230
+ const now = new Date();
231
+ const token = newSecret();
232
+ const expiresAt = new Date(now.getTime() + RESET_TTL_MS);
233
+ db.delete(auth_verification).where(eq(auth_verification.value, userId)).run();
234
+ db.insert(auth_verification)
235
+ .values({
236
+ id: randomUUID(),
237
+ identifier: resetIdentifierOf(token),
238
+ value: userId,
239
+ expiresAt,
240
+ createdAt: now,
241
+ updatedAt: now,
242
+ })
243
+ .run();
244
+ db.update(auth_account)
245
+ .set({ password: scrambledHash, updatedAt: now })
246
+ .where(
247
+ and(
248
+ eq(auth_account.userId, userId),
249
+ eq(auth_account.providerId, CREDENTIAL_PROVIDER),
250
+ ),
251
+ )
252
+ .run();
253
+ revokeUser(store, userId);
254
+ return { token, expiresAt };
255
+ };
256
+
257
+ const disable = (store: IdentityStore, userId: string): void => {
258
+ store.db
259
+ .update(auth_user)
260
+ .set({ disabled: true })
261
+ .where(eq(auth_user.id, userId))
262
+ .run();
263
+ revokeUser(store, userId);
264
+ store.db
265
+ .delete(calendarFeedTokens)
266
+ .where(
267
+ eq(calendarFeedTokens.accountConfigId, deriveAccountConfigId(userId)),
268
+ )
269
+ .run();
270
+ };
271
+
272
+ const enable = (store: IdentityStore, userId: string): void => {
273
+ store.db
274
+ .update(auth_user)
275
+ .set({ disabled: false })
276
+ .where(eq(auth_user.id, userId))
277
+ .run();
278
+ };
279
+
280
+ export const createPeopleDirectory = async (
281
+ config: PeopleConfig,
282
+ ): Promise<PeopleAdmin> => {
283
+ const { store } = config;
284
+ const { password: passwords } = await config.auth.$context;
285
+ const resetUrlOf = (token: string): string =>
286
+ `${config.baseURL}/reset-password/${token}`;
287
+ const scrambledHash = (): Promise<string> => passwords.hash(newSecret());
288
+
289
+ const credentialHash = (userId: string): string | null | undefined =>
290
+ store.db
291
+ .select({ hash: auth_account.password })
292
+ .from(auth_account)
293
+ .where(
294
+ and(
295
+ eq(auth_account.userId, userId),
296
+ eq(auth_account.providerId, CREDENTIAL_PROVIDER),
297
+ ),
298
+ )
299
+ .get()?.hash;
300
+
301
+ const passwordMatches = async (
302
+ userId: string,
303
+ password: string,
304
+ ): Promise<boolean> => {
305
+ const hash = credentialHash(userId);
306
+ if (!hash) return false;
307
+ return passwords.verify({ hash, password });
308
+ };
309
+
310
+ const reserveAttempt = (request: StepUpRequest): PeopleOutcome<string> =>
311
+ store.immediate(() => {
312
+ const callerRefused = callerRefusal(store, request, request.targetId);
313
+ if (callerRefused) return callerRefused;
314
+ if (!findUser(store, request.targetId)) return refused("not_found");
315
+ const failures =
316
+ store.db
317
+ .select({ failures: count() })
318
+ .from(identity_audit_event)
319
+ .where(
320
+ and(
321
+ eq(identity_audit_event.actor, request.callerId),
322
+ eq(identity_audit_event.action, "StepUpFailed"),
323
+ gt(
324
+ identity_audit_event.occurredAt,
325
+ new Date(Date.now() - STEP_UP_WINDOW_MS),
326
+ ),
327
+ ),
328
+ )
329
+ .get()?.failures ?? 0;
330
+ if (failures >= STEP_UP_MAX_FAILURES) return refused("step_up_locked");
331
+ return ok(
332
+ store.audit({
333
+ actor: request.callerId,
334
+ target: request.targetId,
335
+ action: "StepUpFailed",
336
+ outcome: "RefusedStepUp",
337
+ clientIp: request.clientIp,
338
+ }),
339
+ );
340
+ });
341
+
342
+ const grantStepUp = (
343
+ request: StepUpRequest,
344
+ auditEventId: string,
345
+ ): PeopleOutcome<StepUpGrant> =>
346
+ store.immediate(() => {
347
+ const callerRefused = callerRefusal(store, request, request.targetId);
348
+ if (callerRefused) return callerRefused;
349
+ const now = new Date();
350
+ const stepUpToken = newSecret();
351
+ const expiresAt = new Date(now.getTime() + STEP_UP_TTL_MS);
352
+ store.relabel(auditEventId, {
353
+ actor: request.callerId,
354
+ target: request.targetId,
355
+ action: "StepUpPassed",
356
+ outcome: "Succeeded",
357
+ clientIp: request.clientIp,
358
+ });
359
+ store.db
360
+ .delete(identity_step_up)
361
+ .where(lte(identity_step_up.expiresAt, now))
362
+ .run();
363
+ store.db
364
+ .insert(identity_step_up)
365
+ .values({
366
+ digest: digestOf(stepUpToken),
367
+ callerUserId: request.callerId,
368
+ action: request.action,
369
+ targetUserId: request.targetId,
370
+ expiresAt,
371
+ })
372
+ .run();
373
+ return ok({ stepUpToken, expiresAt: expiresAt.getTime() });
374
+ });
375
+
376
+ const directory: PeopleAdmin = {
377
+ listPeople: async (request) =>
378
+ store.immediate(() => {
379
+ const callerRefused = callerRefusal(store, request, "all");
380
+ if (callerRefused) return callerRefused;
381
+ const sessions = activeSessionCounts(store);
382
+ const users = store.db
383
+ .select({
384
+ userId: auth_user.id,
385
+ email: auth_user.email,
386
+ disabled: auth_user.disabled,
387
+ createdAt: auth_user.createdAt,
388
+ lastSignInAt: auth_user.lastSignInAt,
389
+ })
390
+ .from(auth_user)
391
+ .orderBy(asc(auth_user.email))
392
+ .all();
393
+ return ok(
394
+ users.map((user) => toPerson(user, request.callerId, sessions)),
395
+ );
396
+ }),
397
+
398
+ createStepUp: async (request) => {
399
+ const attempt = reserveAttempt(request);
400
+ if (!attempt.ok) return attempt;
401
+ if (!(await passwordMatches(request.callerId, request.password))) {
402
+ return refused("wrong_password");
403
+ }
404
+ return grantStepUp(request, attempt.value);
405
+ },
406
+
407
+ signOutPerson: async (request) =>
408
+ store.immediate(() => {
409
+ const refusal =
410
+ callerRefusal(store, request, request.targetId) ??
411
+ stepUpRefusal(store, request, "SignOut");
412
+ if (refusal) return refusal;
413
+ if (!findUser(store, request.targetId)) return refused("not_found");
414
+ revokeUser(store, request.targetId);
415
+ store.audit({
416
+ actor: request.callerId,
417
+ target: request.targetId,
418
+ action: "SessionsRevoked",
419
+ outcome: "Succeeded",
420
+ clientIp: request.clientIp,
421
+ });
422
+ return ok(personOf(store, request.targetId, request.callerId));
423
+ }),
424
+
425
+ updatePerson: async (request) =>
426
+ store.immediate(() => {
427
+ const disabling = request.status === "Disabled";
428
+ const callerRefused = callerRefusal(store, request, request.targetId);
429
+ if (callerRefused) return callerRefused;
430
+ if (disabling && request.targetId === request.callerId) {
431
+ store.audit({
432
+ actor: request.callerId,
433
+ target: request.targetId,
434
+ action: "UserDisabled",
435
+ outcome: "RefusedSelf",
436
+ clientIp: request.clientIp,
437
+ });
438
+ return refused("cannot_disable_self");
439
+ }
440
+ const stepUpRefused = stepUpRefusal(
441
+ store,
442
+ request,
443
+ disabling ? "Disable" : "Enable",
444
+ );
445
+ if (stepUpRefused) return stepUpRefused;
446
+ if (!findUser(store, request.targetId)) return refused("not_found");
447
+ if (disabling) disable(store, request.targetId);
448
+ else enable(store, request.targetId);
449
+ store.audit({
450
+ actor: request.callerId,
451
+ target: request.targetId,
452
+ action: disabling ? "UserDisabled" : "UserEnabled",
453
+ outcome: "Succeeded",
454
+ clientIp: request.clientIp,
455
+ });
456
+ return ok(personOf(store, request.targetId, request.callerId));
457
+ }),
458
+
459
+ createPasswordReset: async (request) => {
460
+ const scrambled = await scrambledHash();
461
+ const issued = store.immediate(
462
+ (): PeopleOutcome<{ token: string; expiresAt: Date }> => {
463
+ const refusal =
464
+ callerRefusal(store, request, request.targetId) ??
465
+ stepUpRefusal(store, request, "ResetPassword");
466
+ if (refusal) return refusal;
467
+ if (!findUser(store, request.targetId)) return refused("not_found");
468
+ const reset = issueReset(store, request.targetId, scrambled);
469
+ store.audit({
470
+ actor: request.callerId,
471
+ target: request.targetId,
472
+ action: "PasswordResetIssued",
473
+ outcome: "Succeeded",
474
+ clientIp: request.clientIp,
475
+ });
476
+ return ok(reset);
477
+ },
478
+ );
479
+ if (!issued.ok) return issued;
480
+ return ok({
481
+ person: personOf(store, request.targetId, request.callerId),
482
+ resetUrl: resetUrlOf(issued.value.token),
483
+ expiresAt: issued.value.expiresAt.getTime(),
484
+ });
485
+ },
486
+
487
+ recordRefusal: (request) => {
488
+ store.immediate(() =>
489
+ store.audit({
490
+ actor: request.callerId,
491
+ target: request.targetId,
492
+ action: "PeopleRequestRefused",
493
+ outcome: request.outcome,
494
+ clientIp: request.clientIp,
495
+ }),
496
+ );
497
+ },
498
+
499
+ enableByEmail: async (email) =>
500
+ store.immediate(() => {
501
+ const user = userByEmail(store, email);
502
+ if (!user) return refused("not_found");
503
+ enable(store, user.userId);
504
+ store.audit({
505
+ actor: SHELL_ACTOR,
506
+ target: user.userId,
507
+ action: "UserEnabled",
508
+ outcome: "Succeeded",
509
+ clientIp: HOST_CLIENT_IP,
510
+ });
511
+ return ok(personOf(store, user.userId, SHELL_ACTOR));
512
+ }),
513
+
514
+ resetByEmail: async (email) => {
515
+ const scrambled = await scrambledHash();
516
+ return store.immediate(() => {
517
+ const user = userByEmail(store, email);
518
+ if (!user) return refused("not_found");
519
+ const reset = issueReset(store, user.userId, scrambled);
520
+ store.audit({
521
+ actor: SHELL_ACTOR,
522
+ target: user.userId,
523
+ action: "PasswordResetIssued",
524
+ outcome: "Succeeded",
525
+ clientIp: HOST_CLIENT_IP,
526
+ });
527
+ return ok({
528
+ person: personOf(store, user.userId, SHELL_ACTOR),
529
+ resetUrl: resetUrlOf(reset.token),
530
+ expiresAt: reset.expiresAt.getTime(),
531
+ });
532
+ });
533
+ },
534
+
535
+ applySignInSwitch: (signInEnabled) =>
536
+ applySignInSwitch(store, signInEnabled),
537
+
538
+ close: () => store.close(),
539
+ };
540
+ return directory;
541
+ };
@@ -5,6 +5,7 @@ import { join, resolve } from "node:path";
5
5
  import { after, before, describe, it } from "node:test";
6
6
  import type { Auth } from "./auth.js";
7
7
  import { CLIENT_IP_HEADER, createAuth } from "./auth.js";
8
+ import { openIdentityStore } from "./identity-store.js";
8
9
 
9
10
  /**
10
11
  * #441: better-auth answers a rate-limited request from inside its own router,
@@ -15,16 +16,14 @@ import { CLIENT_IP_HEADER, createAuth } from "./auth.js";
15
16
  */
16
17
 
17
18
  process.env.LOG_LEVEL = "info";
18
- process.env.REMIT_SERVICE_NAME = "auth-service-test";
19
19
 
20
20
  const originalWrite = process.stdout.write.bind(process.stdout);
21
21
  const written: string[] = [];
22
22
  let capturing = false;
23
23
 
24
- // pino writes to `process.stdout` only when its `write` has been replaced, and
25
- // to the raw fd otherwise, so the hook must be installed before the module
26
- // under test imports the logger. Writes pass through while nothing is
27
- // capturing, so the test runner's own output still reaches the terminal.
24
+ // The logger writes its lines through `process.stdout`. Writes pass through
25
+ // while nothing is capturing, so the test runner's own output still reaches
26
+ // the terminal.
28
27
  process.stdout.write = ((
29
28
  chunk: string | Uint8Array,
30
29
  ...rest: unknown[]
@@ -41,13 +40,13 @@ const { withRateLimitLogging } = await import("./rate-limit-log.js");
41
40
  const SIGN_IN_MAX = 2;
42
41
  const MIGRATIONS = resolve(
43
42
  import.meta.dirname,
44
- "../../../deploy/vps/migrations-sqlite/auth",
43
+ "../../../deploy/vps/migrations-sqlite/app",
45
44
  );
46
45
 
47
46
  type Line = Record<string, unknown>;
48
47
 
49
48
  // better-auth writes its own advisories through console, so the captured
50
- // stream is not all pino JSON; only the structured lines are of interest here.
49
+ // stream is not all JSON; only the structured lines are of interest here.
51
50
  const asJsonObject = (line: string): Line | null => {
52
51
  try {
53
52
  const parsed: unknown = JSON.parse(line);
@@ -121,13 +120,19 @@ describe("a rate-limited auth response is logged", () => {
121
120
  rmSync(dir, { recursive: true, force: true });
122
121
  });
123
122
 
124
- const build = async (name: string): Promise<Auth> =>
125
- createAuth({
126
- connectionString: await createDatabase(dir, name),
127
- secret: "rate-limit-log-test-secret-value-32chars",
128
- baseURL: "http://localhost:3000",
129
- selfSignUpEnabled: true,
130
- });
123
+ const build = async (name: string): Promise<Auth> => {
124
+ const connectionString = await createDatabase(dir, name);
125
+ return createAuth(
126
+ {
127
+ connectionString,
128
+ secret: "rate-limit-log-test-secret-value-32chars",
129
+ baseURL: "http://localhost:3000",
130
+ selfSignUpEnabled: true,
131
+ signInEnabled: true,
132
+ },
133
+ await openIdentityStore(connectionString),
134
+ );
135
+ };
131
136
 
132
137
  it("names the endpoint and the address the limit is keyed on", async () => {
133
138
  const handler = withRateLimitLogging(await build("logged"));
@@ -1,4 +1,4 @@
1
- import { logger } from "@remit/logger-lambda";
1
+ import { logger } from "@remit/logger";
2
2
  import type { Auth } from "./auth.js";
3
3
  import { CLIENT_IP_HEADER } from "./auth.js";
4
4
  import { AUTH_BASE_PATH } from "./config.js";
@@ -5,11 +5,12 @@ import { join, resolve } from "node:path";
5
5
  import { after, before, describe, it } from "node:test";
6
6
  import type { Auth } from "./auth.js";
7
7
  import { CLIENT_IP_HEADER, createAuth } from "./auth.js";
8
+ import { openIdentityStore } from "./identity-store.js";
8
9
 
9
10
  const SIGN_IN_MAX = 2;
10
11
  const MIGRATIONS = resolve(
11
12
  import.meta.dirname,
12
- "../../../deploy/vps/migrations-sqlite/auth",
13
+ "../../../deploy/vps/migrations-sqlite/app",
13
14
  );
14
15
 
15
16
  const createDatabase = async (dir: string, name: string): Promise<string> => {
@@ -68,13 +69,19 @@ describe("sign-in rate limiting is per client", () => {
68
69
  rmSync(dir, { recursive: true, force: true });
69
70
  });
70
71
 
71
- const build = async (name: string): Promise<Auth> =>
72
- createAuth({
73
- connectionString: await createDatabase(dir, name),
74
- secret: "rate-limit-test-secret-value-32chars-min",
75
- baseURL: "http://localhost:3000",
76
- selfSignUpEnabled: true,
77
- });
72
+ const build = async (name: string): Promise<Auth> => {
73
+ const connectionString = await createDatabase(dir, name);
74
+ return createAuth(
75
+ {
76
+ connectionString,
77
+ secret: "rate-limit-test-secret-value-32chars-min",
78
+ baseURL: "http://localhost:3000",
79
+ selfSignUpEnabled: true,
80
+ signInEnabled: true,
81
+ },
82
+ await openIdentityStore(connectionString),
83
+ );
84
+ };
78
85
 
79
86
  it("locks out only the client that spent the budget", async () => {
80
87
  const auth = await build("per-client");