@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/auth-service",
3
- "version": "0.0.15",
3
+ "version": "0.1.0",
4
4
  "description": "better-auth identity service for the self-host stack",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -17,6 +17,14 @@
17
17
  "./verifier": {
18
18
  "types": "./src/verifier.ts",
19
19
  "default": "./src/verifier.ts"
20
+ },
21
+ "./schema": {
22
+ "types": "./src/schema/auth-schema-sqlite.ts",
23
+ "default": "./src/schema/auth-schema-sqlite.ts"
24
+ },
25
+ "./people": {
26
+ "types": "./src/people.ts",
27
+ "default": "./src/people.ts"
20
28
  }
21
29
  },
22
30
  "scripts": {
@@ -29,12 +37,15 @@
29
37
  "tsx": "*"
30
38
  },
31
39
  "dependencies": {
32
- "@remit/logger-lambda": "*",
33
- "better-auth": "^1.6.23",
40
+ "@remit/logger": "*",
41
+ "better-auth": "^1.7.12",
34
42
  "better-sqlite3": "^12.11.1",
35
43
  "drizzle-orm": "^0.45.2",
36
44
  "jose": "^6.2.3",
37
- "@types/better-sqlite3": "^7.6.13"
45
+ "@types/better-sqlite3": "^7.6.13",
46
+ "@remit/data-ports": "*",
47
+ "@remit/drizzle-sqlite-schema": "*",
48
+ "@remit/api-openapi-types": "*"
38
49
  },
39
50
  "license": "MIT",
40
51
  "publishConfig": {
package/src/auth.ts CHANGED
@@ -1,14 +1,23 @@
1
+ import { logger } from "@remit/logger";
1
2
  import type { Auth as BetterAuthInstance } from "better-auth";
2
- import { betterAuth } from "better-auth";
3
+ import { betterAuth, type GenericEndpointContext } from "better-auth";
3
4
  import { drizzleAdapter } from "better-auth/adapters/drizzle";
5
+ import { APIError, createAuthMiddleware, isAPIError } from "better-auth/api";
4
6
  import { jwt } from "better-auth/plugins";
7
+ import {
8
+ deleteSessions,
9
+ type IdentityStore,
10
+ isUserDisabled,
11
+ markSignedIn,
12
+ sessionUserOf,
13
+ } from "./identity-store.js";
5
14
  import * as authSchemaSqlite from "./schema/auth-schema-sqlite.js";
6
15
 
7
16
  export interface AuthConfig {
8
17
  /**
9
- * Path to the shared database file (RFC 036 D3). The identity tables live
10
- * beside the app tables and are driven through the drizzle adapter's
11
- * `sqlite` provider.
18
+ * Path to the app database file (#1420). The identity tables live beside
19
+ * the app tables and are driven through the drizzle adapter's `sqlite`
20
+ * provider.
12
21
  */
13
22
  connectionString: string;
14
23
  secret: string;
@@ -20,6 +29,7 @@ export interface AuthConfig {
20
29
  * `disableSignUp`; accounts are then provisioned out-of-band.
21
30
  */
22
31
  selfSignUpEnabled: boolean;
32
+ signInEnabled: boolean;
23
33
  }
24
34
 
25
35
  // The drizzle adapter better-auth runs against. better-sqlite3 and its drizzle
@@ -52,6 +62,88 @@ const buildAuthAdapter = async (config: AuthConfig) => {
52
62
  */
53
63
  export const CLIENT_IP_HEADER = "x-remit-client-ip";
54
64
 
65
+ export const SIGN_IN_DISABLED = {
66
+ code: "SIGN_IN_DISABLED",
67
+ message: "Sign-in is turned off on this server.",
68
+ } as const;
69
+
70
+ export const ACCOUNT_DISABLED = {
71
+ code: "ACCOUNT_DISABLED",
72
+ message: "This account is disabled.",
73
+ } as const;
74
+
75
+ const RESET_TOKEN_TTL_SECONDS = 24 * 60 * 60;
76
+
77
+ const UNKNOWN = "unknown";
78
+
79
+ const clientIpOf = (ctx: GenericEndpointContext | null): string =>
80
+ ctx?.request?.headers.get(CLIENT_IP_HEADER) ??
81
+ ctx?.headers?.get(CLIENT_IP_HEADER) ??
82
+ UNKNOWN;
83
+
84
+ const SESSION_PATHS: ReadonlySet<string> = new Set(["/token", "/get-session"]);
85
+
86
+ /**
87
+ * The kill switch, then the disabled check on the two paths that carry a
88
+ * session forward. A sign-in that commits its session just after a disable
89
+ * would otherwise keep minting tokens; here that user's sessions go and the
90
+ * mint is refused.
91
+ */
92
+ const gate = (store: IdentityStore, signInEnabled: boolean) =>
93
+ createAuthMiddleware(async (ctx) => {
94
+ if (!signInEnabled && ctx.path !== "/jwks") {
95
+ throw new APIError("FORBIDDEN", { ...SIGN_IN_DISABLED });
96
+ }
97
+ if (!SESSION_PATHS.has(ctx.path)) return;
98
+ const sessionToken = await ctx.getSignedCookie(
99
+ ctx.context.authCookies.sessionToken.name,
100
+ ctx.context.secret,
101
+ );
102
+ if (!sessionToken) return;
103
+ const user = sessionUserOf(store, sessionToken);
104
+ if (!user?.disabled) return;
105
+ store.immediate(() => {
106
+ deleteSessions(store, user.userId);
107
+ store.audit({
108
+ actor: user.userId,
109
+ target: user.userId,
110
+ action: "SignInRefusedDisabled",
111
+ outcome: "RefusedDisabled",
112
+ clientIp: clientIpOf(ctx),
113
+ });
114
+ });
115
+ throw new APIError("FORBIDDEN", { ...ACCOUNT_DISABLED });
116
+ });
117
+
118
+ /**
119
+ * Every redeem of a reset link leaves a log line carrying its Origin, and a
120
+ * refused one an audit record. A refused token names no user.
121
+ */
122
+ const resetAudit = (store: IdentityStore) =>
123
+ createAuthMiddleware(async (ctx) => {
124
+ if (ctx.path !== "/reset-password") return;
125
+ const refused = isAPIError(ctx.context.returned);
126
+ // biome-ignore lint/plugin/no-logger-info: a reset redeem is an audit-grade signal
127
+ logger.info(
128
+ {
129
+ origin: ctx.request?.headers.get("origin") ?? null,
130
+ clientIp: clientIpOf(ctx),
131
+ accepted: !refused,
132
+ },
133
+ "Password reset redeem",
134
+ );
135
+ if (!refused) return;
136
+ store.immediate(() =>
137
+ store.audit({
138
+ actor: UNKNOWN,
139
+ target: UNKNOWN,
140
+ action: "PasswordResetRefused",
141
+ outcome: "RefusedToken",
142
+ clientIp: clientIpOf(ctx),
143
+ }),
144
+ );
145
+ });
146
+
55
147
  const intEnv = (name: string, fallback: number): number => {
56
148
  const raw = process.env[name];
57
149
  if (!raw) return fallback;
@@ -75,6 +167,7 @@ const intEnv = (name: string, fallback: number): number => {
75
167
  // the public type stable across any consumer's dependency graph.
76
168
  export const createAuth = async (
77
169
  config: AuthConfig,
170
+ store: IdentityStore,
78
171
  ): Promise<BetterAuthInstance> => {
79
172
  const database = await buildAuthAdapter(config);
80
173
 
@@ -87,6 +180,45 @@ export const createAuth = async (
87
180
  enabled: true,
88
181
  autoSignIn: true,
89
182
  disableSignUp: !config.selfSignUpEnabled,
183
+ resetPasswordTokenExpiresIn: RESET_TOKEN_TTL_SECONDS,
184
+ revokeSessionsOnPasswordReset: true,
185
+ onPasswordReset: async ({ user }, request) => {
186
+ store.immediate(() =>
187
+ store.audit({
188
+ actor: user.id,
189
+ target: user.id,
190
+ action: "PasswordResetRedeemed",
191
+ outcome: "Succeeded",
192
+ clientIp: request?.headers.get(CLIENT_IP_HEADER) ?? UNKNOWN,
193
+ }),
194
+ );
195
+ },
196
+ },
197
+ hooks: {
198
+ before: gate(store, config.signInEnabled),
199
+ after: resetAudit(store),
200
+ },
201
+ databaseHooks: {
202
+ session: {
203
+ create: {
204
+ before: async (session, ctx) => {
205
+ if (!isUserDisabled(store.db, session.userId)) return;
206
+ store.immediate(() =>
207
+ store.audit({
208
+ actor: session.userId,
209
+ target: session.userId,
210
+ action: "SignInRefusedDisabled",
211
+ outcome: "RefusedDisabled",
212
+ clientIp: clientIpOf(ctx),
213
+ }),
214
+ );
215
+ throw new APIError("FORBIDDEN", { ...ACCOUNT_DISABLED });
216
+ },
217
+ after: async (session) => {
218
+ markSignedIn(store, session.userId);
219
+ },
220
+ },
221
+ },
90
222
  },
91
223
  // Listing one header, and not better-auth's `x-forwarded-for` default,
92
224
  // is what keeps the resolved address out of a caller's reach: an
@@ -118,10 +250,27 @@ export const createAuth = async (
118
250
  },
119
251
  },
120
252
  },
121
- user: { modelName: "auth_user" },
253
+ user: {
254
+ modelName: "auth_user",
255
+ additionalFields: {
256
+ disabled: {
257
+ type: "boolean",
258
+ required: false,
259
+ defaultValue: false,
260
+ input: false,
261
+ returned: false,
262
+ },
263
+ },
264
+ },
122
265
  session: { modelName: "auth_session" },
123
266
  account: { modelName: "auth_account" },
124
- verification: { modelName: "auth_verification" },
267
+ verification: {
268
+ modelName: "auth_verification",
269
+ storeIdentifier: {
270
+ default: "plain",
271
+ overrides: { "reset-password": "hashed" },
272
+ },
273
+ },
125
274
  plugins: [
126
275
  jwt({
127
276
  schema: { jwks: { modelName: "auth_jwks" } },
package/src/config.ts CHANGED
@@ -11,6 +11,11 @@ const required = (name: string, value: string | undefined): string => {
11
11
  return value;
12
12
  };
13
13
 
14
+ const isSwitchOn = (raw: string | undefined): boolean => {
15
+ const value = (raw ?? "").trim().toLowerCase();
16
+ return value !== "false" && value !== "0" && value !== "no";
17
+ };
18
+
14
19
  /**
15
20
  * Whether self-service signup is open on the better-auth tier. Mirrors the
16
21
  * Cognito user pool's `selfSignUpEnabled` prop: enabled means users can
@@ -23,10 +28,16 @@ const required = (name: string, value: string | undefined): string => {
23
28
  */
24
29
  export const resolveSelfSignUpEnabled = (
25
30
  raw: string | undefined = process.env.SELF_SIGN_UP_ENABLED,
26
- ): boolean => {
27
- const value = (raw ?? "").trim().toLowerCase();
28
- return value !== "false" && value !== "0" && value !== "no";
29
- };
31
+ ): boolean => isSwitchOn(raw);
32
+
33
+ /**
34
+ * The self-host kill switch. `SIGN_IN_ENABLED` reads like
35
+ * `SELF_SIGN_UP_ENABLED`: unset or any other value is on, and an explicit
36
+ * `false`/`0`/`no` turns every sign-in, token mint and session read off.
37
+ */
38
+ export const resolveSignInEnabled = (
39
+ raw: string | undefined = process.env.SIGN_IN_ENABLED,
40
+ ): boolean => isSwitchOn(raw);
30
41
 
31
42
  export interface DataConnectionConfig {
32
43
  connectionString: string;
@@ -34,11 +45,14 @@ export interface DataConnectionConfig {
34
45
 
35
46
  /**
36
47
  * Resolve where identity data lives, from the same env vars the rest of the
37
- * self-host stack reads (RFC 036 D5). The identity tables share the app
38
- * database file, so the locator is `SQLITE_DB_PATH`.
48
+ * self-host stack reads (RFC 036 D5). The identity tables live in the app
49
+ * database, so the locator is `SQLITE_APP_DB_PATH`.
39
50
  */
40
51
  export const resolveDataConnectionConfig = (): DataConnectionConfig => ({
41
- connectionString: required("SQLITE_DB_PATH", process.env.SQLITE_DB_PATH),
52
+ connectionString: required(
53
+ "SQLITE_APP_DB_PATH",
54
+ process.env.SQLITE_APP_DB_PATH,
55
+ ),
42
56
  });
43
57
 
44
58
  /**
@@ -56,6 +70,7 @@ export const resolveAuthConfig = (): AuthConfig => {
56
70
  .map((origin) => origin.trim())
57
71
  .filter((origin) => origin.length > 0),
58
72
  selfSignUpEnabled: resolveSelfSignUpEnabled(),
73
+ signInEnabled: resolveSignInEnabled(),
59
74
  };
60
75
  };
61
76
 
@@ -0,0 +1,286 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { logger } from "@remit/logger";
3
+ import { and, desc, eq, inArray, isNull, lt } from "drizzle-orm";
4
+ import type {
5
+ BetterSQLite3Database,
6
+ drizzle,
7
+ } from "drizzle-orm/better-sqlite3";
8
+ import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
9
+ import {
10
+ auth_session,
11
+ auth_user,
12
+ identity_audit_event,
13
+ identity_step_up,
14
+ } from "./schema/auth-schema-sqlite.js";
15
+
16
+ export type AuditAction =
17
+ | "KillSwitchOn"
18
+ | "KillSwitchOff"
19
+ | "StepUpPassed"
20
+ | "StepUpFailed"
21
+ | "SessionsRevoked"
22
+ | "PasswordResetIssued"
23
+ | "PasswordResetRedeemed"
24
+ | "PasswordResetRefused"
25
+ | "UserDisabled"
26
+ | "UserEnabled"
27
+ | "SignInRefusedDisabled"
28
+ | "PeopleRequestRefused";
29
+
30
+ export type AuditOutcome =
31
+ | "Succeeded"
32
+ | "RefusedSelf"
33
+ | "RefusedCallerDisabled"
34
+ | "RefusedSignInOff"
35
+ | "RefusedForeignOrigin"
36
+ | "RefusedStepUp"
37
+ | "RefusedToken"
38
+ | "RefusedDisabled";
39
+
40
+ export interface AuditRecord {
41
+ actor: string;
42
+ target: string;
43
+ action: AuditAction;
44
+ outcome: AuditOutcome;
45
+ clientIp: string;
46
+ }
47
+
48
+ export const HOST_CLIENT_IP = "host";
49
+
50
+ const AUDIT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
51
+
52
+ // The MCP connector's tables share the app database. auth-service cannot depend
53
+ // on mcp-server, so the four columns a revocation writes are declared here.
54
+ const mcp_oauth_consent = sqliteTable("mcp_oauth_consent", {
55
+ userId: text("user_id"),
56
+ });
57
+ const mcp_oauth_access_token = sqliteTable("mcp_oauth_access_token", {
58
+ userId: text("user_id"),
59
+ revoked: integer("revoked", { mode: "timestamp_ms" }),
60
+ });
61
+ const mcp_oauth_refresh_token = sqliteTable("mcp_oauth_refresh_token", {
62
+ userId: text("user_id"),
63
+ revoked: integer("revoked", { mode: "timestamp_ms" }),
64
+ });
65
+ const mcp_session = sqliteTable("mcp_session", {
66
+ userId: text("user_id"),
67
+ });
68
+
69
+ export type IdentityDb = ReturnType<typeof drizzle>;
70
+
71
+ type SqliteDb = BetterSQLite3Database<Record<string, unknown>>;
72
+
73
+ export interface IdentityStore {
74
+ db: IdentityDb;
75
+ immediate<T>(work: () => T): T;
76
+ audit(record: AuditRecord): string;
77
+ relabel(auditEventId: string, record: AuditRecord): void;
78
+ close(): void;
79
+ }
80
+
81
+ const logAudit = (record: AuditRecord): void => {
82
+ // biome-ignore lint/plugin/no-logger-info: an identity audit record is an audit-grade signal
83
+ logger.info({ audit: record }, "Identity audit event");
84
+ };
85
+
86
+ const insertAudit = (db: SqliteDb, record: AuditRecord): string => {
87
+ const now = Date.now();
88
+ const auditEventId = randomUUID();
89
+ db.delete(identity_audit_event)
90
+ .where(
91
+ lt(identity_audit_event.occurredAt, new Date(now - AUDIT_RETENTION_MS)),
92
+ )
93
+ .run();
94
+ db.insert(identity_audit_event)
95
+ .values({ auditEventId, occurredAt: new Date(now), ...record })
96
+ .run();
97
+ return auditEventId;
98
+ };
99
+
100
+ export const writeAudit = (db: SqliteDb, record: AuditRecord): void => {
101
+ insertAudit(db, record);
102
+ logAudit(record);
103
+ };
104
+
105
+ export const identityStore = (db: IdentityDb): IdentityStore => {
106
+ let pending: AuditRecord[] | null = null;
107
+ const sqlite = db.$client;
108
+
109
+ const log = (record: AuditRecord): void => {
110
+ if (pending) pending.push(record);
111
+ else logAudit(record);
112
+ };
113
+
114
+ const audit = (record: AuditRecord): string => {
115
+ const auditEventId = insertAudit(db, record);
116
+ log(record);
117
+ return auditEventId;
118
+ };
119
+
120
+ const relabel = (auditEventId: string, record: AuditRecord): void => {
121
+ db.update(identity_audit_event)
122
+ .set({ action: record.action, outcome: record.outcome })
123
+ .where(eq(identity_audit_event.auditEventId, auditEventId))
124
+ .run();
125
+ log(record);
126
+ };
127
+
128
+ const immediate = <T>(work: () => T): T => {
129
+ pending = [];
130
+ const written = pending;
131
+ const result = (() => {
132
+ try {
133
+ return sqlite.transaction(work).immediate();
134
+ } finally {
135
+ pending = null;
136
+ }
137
+ })();
138
+ for (const record of written) logAudit(record);
139
+ return result;
140
+ };
141
+
142
+ return { db, immediate, audit, relabel, close: () => sqlite.close() };
143
+ };
144
+
145
+ export const openIdentityStore = async (
146
+ filename: string,
147
+ ): Promise<IdentityStore> => {
148
+ const { default: Database } = await import("better-sqlite3");
149
+ const { drizzle } = await import("drizzle-orm/better-sqlite3");
150
+ const sqlite = new Database(filename);
151
+ sqlite.pragma("journal_mode = WAL");
152
+ sqlite.pragma("busy_timeout = 5000");
153
+ sqlite.pragma("synchronous = NORMAL");
154
+ sqlite.pragma("foreign_keys = ON");
155
+ return identityStore(drizzle(sqlite));
156
+ };
157
+
158
+ export interface UserRow {
159
+ userId: string;
160
+ email: string;
161
+ disabled: boolean;
162
+ createdAt: Date;
163
+ lastSignInAt: Date;
164
+ }
165
+
166
+ export const findUser = (
167
+ store: IdentityStore,
168
+ userId: string,
169
+ ): UserRow | undefined =>
170
+ store.db
171
+ .select({
172
+ userId: auth_user.id,
173
+ email: auth_user.email,
174
+ disabled: auth_user.disabled,
175
+ createdAt: auth_user.createdAt,
176
+ lastSignInAt: auth_user.lastSignInAt,
177
+ })
178
+ .from(auth_user)
179
+ .where(eq(auth_user.id, userId))
180
+ .get();
181
+
182
+ export const isUserDisabled = (db: SqliteDb, userId: string): boolean =>
183
+ db
184
+ .select({ disabled: auth_user.disabled })
185
+ .from(auth_user)
186
+ .where(eq(auth_user.id, userId))
187
+ .get()?.disabled === true;
188
+
189
+ export const sessionUserOf = (
190
+ store: IdentityStore,
191
+ sessionToken: string,
192
+ ): { userId: string; disabled: boolean } | undefined =>
193
+ store.db
194
+ .select({ userId: auth_user.id, disabled: auth_user.disabled })
195
+ .from(auth_session)
196
+ .innerJoin(auth_user, eq(auth_user.id, auth_session.userId))
197
+ .where(eq(auth_session.token, sessionToken))
198
+ .get();
199
+
200
+ export const markSignedIn = (store: IdentityStore, userId: string): void => {
201
+ store.db
202
+ .update(auth_user)
203
+ .set({ lastSignInAt: new Date() })
204
+ .where(eq(auth_user.id, userId))
205
+ .run();
206
+ };
207
+
208
+ export const deleteSessions = (store: IdentityStore, userId: string): void => {
209
+ store.db.delete(auth_session).where(eq(auth_session.userId, userId)).run();
210
+ };
211
+
212
+ export const revokeUser = (store: IdentityStore, userId: string): void => {
213
+ const { db } = store;
214
+ const revoked = new Date();
215
+ deleteSessions(store, userId);
216
+ db.delete(mcp_oauth_consent)
217
+ .where(eq(mcp_oauth_consent.userId, userId))
218
+ .run();
219
+ db.update(mcp_oauth_access_token)
220
+ .set({ revoked })
221
+ .where(
222
+ and(
223
+ eq(mcp_oauth_access_token.userId, userId),
224
+ isNull(mcp_oauth_access_token.revoked),
225
+ ),
226
+ )
227
+ .run();
228
+ db.update(mcp_oauth_refresh_token)
229
+ .set({ revoked })
230
+ .where(
231
+ and(
232
+ eq(mcp_oauth_refresh_token.userId, userId),
233
+ isNull(mcp_oauth_refresh_token.revoked),
234
+ ),
235
+ )
236
+ .run();
237
+ db.delete(mcp_session).where(eq(mcp_session.userId, userId)).run();
238
+ db.delete(identity_step_up)
239
+ .where(eq(identity_step_up.callerUserId, userId))
240
+ .run();
241
+ };
242
+
243
+ const revokeEveryone = (store: IdentityStore): void => {
244
+ const { db } = store;
245
+ const revoked = new Date();
246
+ db.delete(auth_session).run();
247
+ db.delete(mcp_oauth_consent).run();
248
+ db.update(mcp_oauth_access_token)
249
+ .set({ revoked })
250
+ .where(isNull(mcp_oauth_access_token.revoked))
251
+ .run();
252
+ db.update(mcp_oauth_refresh_token)
253
+ .set({ revoked })
254
+ .where(isNull(mcp_oauth_refresh_token.revoked))
255
+ .run();
256
+ db.delete(mcp_session).run();
257
+ db.delete(identity_step_up).run();
258
+ };
259
+
260
+ export const applySignInSwitch = (
261
+ store: IdentityStore,
262
+ signInEnabled: boolean,
263
+ ): void => {
264
+ store.immediate(() => {
265
+ if (!signInEnabled) revokeEveryone(store);
266
+ const latest = store.db
267
+ .select({ action: identity_audit_event.action })
268
+ .from(identity_audit_event)
269
+ .where(
270
+ inArray(identity_audit_event.action, ["KillSwitchOn", "KillSwitchOff"]),
271
+ )
272
+ .orderBy(desc(identity_audit_event.occurredAt))
273
+ .limit(1)
274
+ .get();
275
+ const state: AuditAction = signInEnabled ? "KillSwitchOff" : "KillSwitchOn";
276
+ const changed = latest ? latest.action !== state : !signInEnabled;
277
+ if (!changed) return;
278
+ store.audit({
279
+ actor: "env",
280
+ target: "all",
281
+ action: state,
282
+ outcome: "Succeeded",
283
+ clientIp: HOST_CLIENT_IP,
284
+ });
285
+ });
286
+ };
package/src/index.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export { toNodeHandler } from "better-auth/node";
2
2
  export {
3
+ ACCOUNT_DISABLED,
3
4
  type Auth,
4
5
  type AuthConfig,
5
6
  CLIENT_IP_HEADER,
6
7
  createAuth,
8
+ SIGN_IN_DISABLED,
7
9
  } from "./auth.js";
8
10
  export {
9
11
  AUTH_BASE_PATH,
@@ -13,9 +15,24 @@ export {
13
15
  resolveAuthConfig,
14
16
  resolveDataConnectionConfig,
15
17
  resolveSelfSignUpEnabled,
18
+ resolveSignInEnabled,
16
19
  resolveVerifierConfig,
17
20
  type VerifierConfig,
18
21
  } from "./config.js";
22
+ export {
23
+ type AuditRecord,
24
+ HOST_CLIENT_IP,
25
+ type IdentityStore,
26
+ isUserDisabled,
27
+ openIdentityStore,
28
+ writeAudit,
29
+ } from "./identity-store.js";
30
+ export {
31
+ createPeopleDirectory,
32
+ type PeopleAdmin,
33
+ type PeopleConfig,
34
+ type PeopleDirectory,
35
+ } from "./people.js";
19
36
  export {
20
37
  type AuthFetchHandler,
21
38
  withRateLimitLogging,