@getstrata/bootstrap 1.0.8 → 1.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.
@@ -2,6 +2,7 @@
2
2
  // ../../src/bootstrap/metricsRoutes.ts
3
3
  import { timingSafeEqual } from "crypto";
4
4
  import { prometheusRegistry } from "@getstrata/core/metrics/prometheus";
5
+ import { isProductionEnv } from "@getstrata/core/runtime/appEnv";
5
6
  function tokensMatch(left, right) {
6
7
  const leftBuffer = Buffer.from(left);
7
8
  const rightBuffer = Buffer.from(right);
@@ -17,7 +18,7 @@ function authorizeMetrics(request) {
17
18
  if (expected) {
18
19
  return presented.length > 0 && tokensMatch(presented, expected);
19
20
  }
20
- if ((process.env.APP_ENV ?? "local") === "production") {
21
+ if (isProductionEnv()) {
21
22
  return false;
22
23
  }
23
24
  return true;
@@ -14,7 +14,7 @@ import { SessionGuard } from "@getstrata/core/auth/sessionGuard";
14
14
  import { envFlagEnabled } from "@getstrata/core/runtime/appEnv";
15
15
  var authConfig = {
16
16
  allowDevHeaders: envFlagEnabled(process.env.AUTH_DEV_HEADERS),
17
- tokenDefaultAbilities: ["*"]
17
+ tokenDefaultAbilities: []
18
18
  };
19
19
 
20
20
  // ../../src/bootstrap/config.ts
@@ -50,7 +50,7 @@ var authProvider = {
50
50
  config.set("auth.allowDevHeaders", authConfig.allowDevHeaders);
51
51
  const apiGuard = new DatabaseTokenGuard(container);
52
52
  const sessionGuard = new SessionGuard(container);
53
- const jwtGuard = new JwtGuard;
53
+ const jwtGuard = new JwtGuard(container);
54
54
  const basicGuard = new BasicAuthGuard(container);
55
55
  const guards = [apiGuard, jwtGuard, basicGuard, sessionGuard];
56
56
  if (authConfig.allowDevHeaders) {
@@ -95,7 +95,7 @@ import { validateEnv } from "@getstrata/core/config/envSchema";
95
95
  var appConfig = {
96
96
  name: process.env.APP_NAME?.trim() || "Strata",
97
97
  env: process.env.APP_ENV ?? "local",
98
- debug: (process.env.APP_DEBUG ?? "true") !== "false",
98
+ debug: (process.env.APP_DEBUG ?? "false") === "true",
99
99
  url: process.env.APP_URL ?? "http://localhost:3000",
100
100
  apiPrefix: process.env.API_PREFIX ?? "/api/v1"
101
101
  };
@@ -14,8 +14,8 @@ function readFeatureFlags() {
14
14
  samlLogin: (process.env.FEATURE_SAML ?? "false") === "true",
15
15
  scim: (process.env.FEATURE_SCIM ?? "true") !== "false",
16
16
  billing: (process.env.FEATURE_BILLING ?? "true") !== "false",
17
- siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "true") !== "false",
18
- publicReads: (process.env.FEATURE_PUBLIC_READS ?? "true") !== "false",
17
+ siemExport: (process.env.FEATURE_SIEM_EXPORT ?? "false") === "true",
18
+ publicReads: (process.env.FEATURE_PUBLIC_READS ?? "false") === "true",
19
19
  emailVerification: (process.env.FEATURE_EMAIL_VERIFICATION ?? "false") === "true",
20
20
  mfa: (process.env.FEATURE_MFA ?? "false") === "true",
21
21
  registration: (process.env.FEATURE_REGISTRATION ?? "true") !== "false"
@@ -1,7 +1,15 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/secretsGuard.ts
3
+ import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
3
4
  import { envFlagEnabled, isProductionEnv } from "@getstrata/core/runtime/appEnv";
4
5
  import { isViewsMode, parseFrontendMode } from "@getstrata/core/runtime/frontendMode";
6
+ import {
7
+ assertPostgresRoleCannotBypassRls,
8
+ inspectCurrentPostgresRole,
9
+ isPostgresUrl,
10
+ postgresUrlUsername
11
+ } from "@getstrata/core/tenant/enableTenantRls";
12
+ import { isRlsTenancy } from "@getstrata/core/tenant/tenancyConfig";
5
13
  var PUBLISHED_TEST_ADMIN_API_TOKEN = "strata-admin-test-token";
6
14
  var PUBLISHED_TEST_MEMBER_API_TOKEN = "strata-member-test-token";
7
15
  var PUBLISHED_TEST_SCIM_BEARER_TOKEN = "strata-scim-test-token";
@@ -22,8 +30,11 @@ var SECRETS_TO_ROTATE = [
22
30
  "KMS_ENCRYPTION_KEY",
23
31
  "STRIPE_WEBHOOK_SECRET",
24
32
  "ADMIN_API_TOKEN",
25
- "MEMBER_API_TOKEN"
33
+ "MEMBER_API_TOKEN",
34
+ "DATABASE_URL",
35
+ "APP_DATABASE_URL"
26
36
  ];
37
+ var RLS_BYPASS_DATABASE_USERS = new Set(["postgres", "root"]);
27
38
  function assertNoPlaceholderSecrets(env) {
28
39
  const unrotated = SECRETS_TO_ROTATE.filter((name) => PLACEHOLDER_SECRET_PATTERN.test(env[name]?.trim() ?? ""));
29
40
  if (unrotated.length > 0) {
@@ -34,7 +45,11 @@ function isTokenAuthEnabled(env) {
34
45
  return Boolean(env.ADMIN_API_TOKEN?.trim()) || Boolean(env.MEMBER_API_TOKEN?.trim()) || envFlagEnabled(env.FEATURE_API_TOKENS);
35
46
  }
36
47
  function isOAuthEnabled(env) {
37
- return envFlagEnabled(env.FEATURE_OAUTH) || envFlagEnabled(env.FEATURE_SAML) || Boolean(env.GITHUB_CLIENT_ID?.trim()) || Boolean(env.OIDC_ISSUER?.trim()) || Boolean(env.SAML_LOGIN_URL?.trim());
48
+ return envFlagEnabled(env.FEATURE_OAUTH) || envFlagEnabled(env.FEATURE_SAML) || Boolean(env.GITHUB_CLIENT_ID?.trim()) || Boolean(env.OIDC_ISSUER?.trim()) || Boolean(env.SAML_IDP_SSO_URL?.trim()) || Boolean(env.SAML_LOGIN_URL?.trim());
49
+ }
50
+ function isHeaderOnlyAuth(env) {
51
+ const frontend = env.FRONTEND_MODE?.trim() ?? "";
52
+ return env.AUTH_MODE === "headers" || frontend === "api" && env.AUTH_DEV_HEADERS === "true";
38
53
  }
39
54
  function isCorsConfigured(env) {
40
55
  return env.CORS_ALLOWED_ORIGINS !== undefined;
@@ -77,8 +92,27 @@ function assertFeatureProductionSecrets(env) {
77
92
  throw new Error("Production startup blocked: rotate SCIM_BEARER_TOKEN away from published test defaults.");
78
93
  }
79
94
  }
80
- if (envFlagEnabled(env.FEATURE_FIELD_ENCRYPTION) && !env.KMS_ENCRYPTION_KEY?.trim()) {
81
- throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption is enabled.");
95
+ if ((envFlagEnabled(env.FEATURE_FIELD_ENCRYPTION) || envFlagEnabled(env.FEATURE_MFA)) && !env.KMS_ENCRYPTION_KEY?.trim()) {
96
+ throw new Error("Production startup blocked: set KMS_ENCRYPTION_KEY when field encryption or MFA is enabled.");
97
+ }
98
+ if (envFlagEnabled(env.FEATURE_SAML)) {
99
+ const required = [
100
+ "SAML_IDP_CERT",
101
+ "SAML_IDP_SSO_URL",
102
+ "SAML_SP_ENTITY_ID",
103
+ "SAML_ACS_URL",
104
+ "SAML_IDP_ISSUER"
105
+ ];
106
+ const missing = required.filter((name) => !env[name]?.trim());
107
+ if (missing.length > 0) {
108
+ throw new Error(`Production startup blocked: set ${missing.join(", ")} when FEATURE_SAML=true.`);
109
+ }
110
+ if (env.SAML_WANT_RESPONSE_SIGNED === "false") {
111
+ throw new Error("Production startup blocked: signed SAML responses are required (do not set SAML_WANT_RESPONSE_SIGNED=false).");
112
+ }
113
+ }
114
+ if (envFlagEnabled(env.UPLOAD_ALLOW_UNKNOWN_MIME)) {
115
+ throw new Error("Production startup blocked: set UPLOAD_ALLOW_UNKNOWN_MIME=false (unknown MIME types are not allowed).");
82
116
  }
83
117
  if (envFlagEnabled(env.FEATURE_BILLING) && !env.STRIPE_WEBHOOK_SECRET?.trim()) {
84
118
  throw new Error("Production startup blocked: set STRIPE_WEBHOOK_SECRET when billing webhooks are enabled.");
@@ -100,6 +134,61 @@ function assertFeatureProductionSecrets(env) {
100
134
  }
101
135
  }
102
136
  var LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"]);
137
+ var livePostgresRoleInspector = null;
138
+ function setLivePostgresRoleInspectorForTests(inspect) {
139
+ livePostgresRoleInspector = inspect;
140
+ }
141
+ function rlsRuntimeDatabaseUrl(env) {
142
+ const app = env.APP_DATABASE_URL?.trim();
143
+ if (app) {
144
+ return { name: "APP_DATABASE_URL", raw: app };
145
+ }
146
+ const database = env.DATABASE_URL?.trim();
147
+ if (database) {
148
+ return { name: "DATABASE_URL", raw: database };
149
+ }
150
+ return null;
151
+ }
152
+ function assertRlsUsesAppDatabaseRole(env) {
153
+ if (!isRlsTenancy(env)) {
154
+ return;
155
+ }
156
+ const runtime = rlsRuntimeDatabaseUrl(env);
157
+ if (!runtime) {
158
+ return;
159
+ }
160
+ const user = postgresUrlUsername(runtime.raw);
161
+ if (user === null) {
162
+ return;
163
+ }
164
+ if (user === "") {
165
+ throw new Error(`TENANCY_DRIVER=rls startup blocked: ${runtime.name} must include a NOBYPASSRLS role username.`);
166
+ }
167
+ if (RLS_BYPASS_DATABASE_USERS.has(user.toLowerCase())) {
168
+ throw new Error(`TENANCY_DRIVER=rls startup blocked: ${runtime.name} for TENANCY_DRIVER=rls must use a NOBYPASSRLS role, not ${user}. FORCE RLS does not apply to PostgreSQL superusers.`);
169
+ }
170
+ }
171
+ async function defaultInspectLivePostgresRole() {
172
+ return await inspectCurrentPostgresRole(getDefaultDatabasePool());
173
+ }
174
+ async function assertRlsLiveDatabaseRole(env = process.env, inspect) {
175
+ if (!isRlsTenancy(env)) {
176
+ return;
177
+ }
178
+ const runtime = rlsRuntimeDatabaseUrl(env);
179
+ if (!runtime || !isPostgresUrl(runtime.raw)) {
180
+ return;
181
+ }
182
+ const source = runtime.name;
183
+ assertRlsUsesAppDatabaseRole(env);
184
+ let role;
185
+ try {
186
+ role = await (inspect ?? livePostgresRoleInspector ?? defaultInspectLivePostgresRole)();
187
+ } catch {
188
+ throw new Error(`TENANCY_DRIVER=rls startup blocked: could not inspect the live Postgres role for ${source}.`);
189
+ }
190
+ assertPostgresRoleCannotBypassRls(role, source);
191
+ }
103
192
  function assertPublicAppUrl(env) {
104
193
  const raw = env.APP_URL?.trim() ?? "";
105
194
  let url = null;
@@ -122,6 +211,9 @@ function assertProductionSecrets(env = process.env) {
122
211
  assertNoPlaceholderSecrets(env);
123
212
  assertAuthDevHeadersDisabled(env);
124
213
  assertPublicAppUrl(env);
214
+ if (isHeaderOnlyAuth(env)) {
215
+ throw new Error("Production startup blocked: header-only authentication is not allowed. Configure cookie, token, or JWT auth.");
216
+ }
125
217
  if (isTokenAuthEnabled(env)) {
126
218
  assertTokenAuthProductionSecrets(env);
127
219
  }
@@ -129,7 +221,10 @@ function assertProductionSecrets(env = process.env) {
129
221
  if (isViewsMode(parseFrontendMode(env.FRONTEND_MODE))) {
130
222
  assertSessionSecret(env);
131
223
  }
224
+ assertRlsUsesAppDatabaseRole(env);
132
225
  }
133
226
  export {
134
- assertProductionSecrets
227
+ assertProductionSecrets,
228
+ assertRlsLiveDatabaseRole,
229
+ setLivePostgresRoleInspectorForTests
135
230
  };
@@ -144,8 +144,9 @@ class HttpKernel {
144
144
  case "web":
145
145
  return isViewsEnabled() ? [createFlashMiddleware(), createCsrfMiddleware()] : [];
146
146
  case "api": {
147
+ const csrf = createCsrfMiddleware();
147
148
  if (!this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
148
- return [];
149
+ return [csrf];
149
150
  }
150
151
  const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
151
152
  const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
@@ -155,7 +156,8 @@ class HttpKernel {
155
156
  createMemoryThrottleMiddleware({
156
157
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
157
158
  decaySeconds: 60
158
- })
159
+ }),
160
+ csrf
159
161
  ];
160
162
  }
161
163
  const maxAttempts = Number(process.env.RATE_LIMIT_PER_MINUTE ?? "120");
@@ -164,7 +166,8 @@ class HttpKernel {
164
166
  redisUrl,
165
167
  maxAttempts: Number.isFinite(maxAttempts) ? maxAttempts : 120,
166
168
  decaySeconds: 60
167
- })
169
+ }),
170
+ csrf
168
171
  ];
169
172
  }
170
173
  default:
@@ -174,13 +177,14 @@ class HttpKernel {
174
177
  wrap(groups, handler) {
175
178
  const names = Array.isArray(groups) ? groups : [groups];
176
179
  const middleware = names.flatMap((name) => this.group(name));
177
- if (middleware.length === 0) {
178
- return handler;
180
+ const wrapped = middleware.length === 0 ? handler : withMiddleware(...middleware)(handler);
181
+ if (names.includes("api")) {
182
+ return withJsonErrorHandling(wrapped);
179
183
  }
180
- return withMiddleware(...middleware)(handler);
184
+ return wrapped;
181
185
  }
182
186
  wrapApi(handler) {
183
- return withJsonErrorHandling(this.wrap(["api", "authenticated"], handler));
187
+ return this.wrap(["api", "authenticated"], handler);
184
188
  }
185
189
  wrapWeb(handler) {
186
190
  return withErrorHandling(this.wrap("web", handler));
@@ -289,41 +293,28 @@ class HttpKernel {
289
293
  return [createRequireVerifiedMiddleware(auth)];
290
294
  }
291
295
  wrapThrottle(scope, rateLimit, handler) {
292
- const middleware = [];
293
296
  const memoryKeyPrefix = scope === "login" ? "login-throttle:" : "register-throttle:";
294
- if (this.dependencies.container.has(CORE_CONFIG_TOKEN)) {
295
- const config = this.dependencies.container.resolve(CORE_CONFIG_TOKEN);
296
- const redisUrl = config.get(REDIS_URL_CONFIG_KEY)?.trim() ?? "";
297
- if (redisUrl) {
298
- const throttle = scope === "login" ? createLoginThrottleMiddleware({
299
- redisUrl,
300
- maxAttempts: rateLimit.maxAttempts,
301
- decaySeconds: rateLimit.decaySeconds
302
- }) : createThrottleMiddleware({
303
- redisUrl,
304
- maxAttempts: rateLimit.maxAttempts,
305
- decaySeconds: rateLimit.decaySeconds,
306
- keyPrefix: memoryKeyPrefix
307
- });
308
- middleware.push(throttle);
309
- } else {
310
- middleware.push(createMemoryThrottleMiddleware({
311
- maxAttempts: rateLimit.maxAttempts,
312
- decaySeconds: rateLimit.decaySeconds,
313
- keyPrefix: memoryKeyPrefix
314
- }));
315
- }
316
- } else {
317
- middleware.push(createMemoryThrottleMiddleware({
297
+ const redisUrl = this.dependencies.container.has(CORE_CONFIG_TOKEN) ? this.dependencies.container.resolve(CORE_CONFIG_TOKEN).get(REDIS_URL_CONFIG_KEY)?.trim() ?? "" : "";
298
+ if (scope === "login") {
299
+ return withMiddleware(createLoginThrottleMiddleware({
300
+ ...redisUrl ? { redisUrl } : {},
301
+ maxAttempts: rateLimit.maxAttempts,
302
+ decaySeconds: rateLimit.decaySeconds
303
+ }))(handler);
304
+ }
305
+ if (redisUrl) {
306
+ return withMiddleware(createThrottleMiddleware({
307
+ redisUrl,
318
308
  maxAttempts: rateLimit.maxAttempts,
319
309
  decaySeconds: rateLimit.decaySeconds,
320
310
  keyPrefix: memoryKeyPrefix
321
- }));
322
- }
323
- if (middleware.length === 0) {
324
- return handler;
311
+ }))(handler);
325
312
  }
326
- return withMiddleware(...middleware)(handler);
313
+ return withMiddleware(createMemoryThrottleMiddleware({
314
+ maxAttempts: rateLimit.maxAttempts,
315
+ decaySeconds: rateLimit.decaySeconds,
316
+ keyPrefix: memoryKeyPrefix
317
+ }))(handler);
327
318
  }
328
319
  }
329
320
  function createHttpKernel(dependencies) {
@@ -1,6 +1,7 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/web/server.ts
3
3
  import { currentRequestMeta, runWithRequestMeta } from "@getstrata/core/http/requestMetaContext";
4
+ import { assertUrlPathUnderRoot } from "@getstrata/core/security/safePath";
4
5
  import { notFoundHtmlResponse } from "@getstrata/core/view";
5
6
  function socketAddress(server, request) {
6
7
  const address = server?.requestIP(request)?.address;
@@ -60,9 +61,14 @@ function createWebServer(options) {
60
61
  await options.onRequest?.(request);
61
62
  const url = new URL(request.url);
62
63
  if (url.pathname.startsWith("/assets/")) {
63
- const file = Bun.file(`${publicDir}${url.pathname}`);
64
- if (await file.exists()) {
65
- return new Response(file);
64
+ try {
65
+ const filePath = assertUrlPathUnderRoot(publicDir, url.pathname);
66
+ const file = Bun.file(filePath);
67
+ if (await file.exists()) {
68
+ return new Response(file);
69
+ }
70
+ } catch {
71
+ return await missingHtmlResponse();
66
72
  }
67
73
  }
68
74
  if (options.handle) {
@@ -2,13 +2,22 @@
2
2
  // ../../src/bootstrap/web/session.ts
3
3
  import { createHmac, randomBytes } from "crypto";
4
4
  import { AuthManager } from "@getstrata/core/auth/guard";
5
+ import { isSessionInvalidated } from "@getstrata/core/auth/sessionCookie";
5
6
  import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
6
- import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
7
+ import {
8
+ getActiveDatabaseConnection,
9
+ hasActiveDatabaseConnection
10
+ } from "@getstrata/core/database/connectionContext";
11
+ import {
12
+ getDefaultDatabasePool,
13
+ getDefaultDatabaseQuery
14
+ } from "@getstrata/core/database/defaultConnection";
7
15
  import { currentSqlDialect, sqlTimestamp } from "@getstrata/core/database/dialect";
8
16
  import { readRequestCookie } from "@getstrata/core/http/cookies";
9
17
  import { currentRequestMeta } from "@getstrata/core/http/requestMetaContext";
10
18
  import { isProductionEnv } from "@getstrata/core/runtime/appEnv";
11
19
  import { timingSafeCompareString } from "@getstrata/core/security/timingSafeCompare";
20
+ import { runWithMigrationBypassForIdentifier } from "@getstrata/core/tenant/databaseTenantContext";
12
21
  function sqlPlaceholder(index) {
13
22
  return currentSqlDialect().placeholder(index);
14
23
  }
@@ -19,10 +28,18 @@ function isSqlClient(value) {
19
28
  return typeof value.unsafe === "function";
20
29
  }
21
30
  function resolveSql(source) {
22
- if (isSqlClient(source)) {
23
- return source;
31
+ const client = isSqlClient(source) ? source : source();
32
+ if (!hasActiveDatabaseConnection()) {
33
+ return client;
24
34
  }
25
- return source();
35
+ try {
36
+ if (client === getDefaultDatabasePool() || client === getDefaultDatabaseQuery()) {
37
+ return getActiveDatabaseConnection(client);
38
+ }
39
+ } catch {
40
+ return client;
41
+ }
42
+ return client;
26
43
  }
27
44
  function defaultSessionSql() {
28
45
  const bound = getBoundDatabaseConnection();
@@ -59,14 +76,32 @@ function mapSessionUserRow(row) {
59
76
  };
60
77
  }
61
78
  async function defaultLoadSessionUser(sql, sessionId) {
62
- const rows = await sql.unsafe(`SELECT s.user_id, s.expires_at, u.*
63
- FROM sessions s
64
- INNER JOIN users u ON u.id = s.user_id
65
- WHERE s.id = ${sqlPlaceholder(1)} AND s.expires_at > ${sqlNow()}`, [sessionId]);
66
- const row = rows[0];
67
- if (!row)
79
+ const sessionRows = await runWithMigrationBypassForIdentifier(sessionId, async () => {
80
+ return await sql.unsafe(`SELECT user_id, expires_at, created_at AS session_created_at
81
+ FROM sessions
82
+ WHERE id = ${sqlPlaceholder(1)} AND expires_at > ${sqlNow()}`, [sessionId]);
83
+ });
84
+ const session = sessionRows[0];
85
+ if (!session)
86
+ return null;
87
+ const userId = Number(session.user_id);
88
+ if (!Number.isInteger(userId) || userId <= 0) {
68
89
  return null;
69
- return mapSessionUserRow(row);
90
+ }
91
+ return await runWithMigrationBypassForIdentifier(userId, async () => {
92
+ const rows = await sql.unsafe(`SELECT * FROM users WHERE id = ${sqlPlaceholder(1)}`, [
93
+ userId
94
+ ]);
95
+ const row = rows[0];
96
+ if (!row)
97
+ return null;
98
+ const createdSource = session.session_created_at;
99
+ const createdAt = createdSource instanceof Date ? createdSource.getTime() : createdSource ? Date.parse(String(createdSource)) : Number.NaN;
100
+ if (!Number.isFinite(createdAt) || isSessionInvalidated(createdAt, row.session_valid_after)) {
101
+ return null;
102
+ }
103
+ return mapSessionUserRow({ ...row, user_id: userId, session_created_at: createdSource });
104
+ });
70
105
  }
71
106
  function redirectWithCookie(location, setCookie, status) {
72
107
  return new Response(null, {
@@ -121,25 +156,40 @@ class CookieSessionStore {
121
156
  async create(user, meta = {}) {
122
157
  const id = randomBytes(32).toString("hex");
123
158
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
124
- await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address, last_active_at)
125
- VALUES (${sqlPlaceholder(1)}, ${sqlPlaceholder(2)}, ${sqlPlaceholder(3)}, ${sqlPlaceholder(4)}, ${sqlPlaceholder(5)}, ${sqlNow()})`, [id, user.id, sqlTimestamp(expires), meta.userAgent ?? null, meta.ipAddress ?? null]);
159
+ await runWithMigrationBypassForIdentifier(user.id, async () => {
160
+ await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at, user_agent, ip_address, last_active_at, created_at)
161
+ VALUES (${sqlPlaceholder(1)}, ${sqlPlaceholder(2)}, ${sqlPlaceholder(3)}, ${sqlPlaceholder(4)}, ${sqlPlaceholder(5)}, ${sqlNow()}, ${sqlNow()})`, [id, user.id, sqlTimestamp(expires), meta.userAgent ?? null, meta.ipAddress ?? null]);
162
+ });
126
163
  return id;
127
164
  }
128
165
  async destroy(sessionId) {
129
- await this.sql().unsafe(`DELETE FROM sessions WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
166
+ await runWithMigrationBypassForIdentifier(sessionId, async () => {
167
+ await this.sql().unsafe(`DELETE FROM sessions WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
168
+ });
169
+ }
170
+ async destroyAllSessions(userId) {
171
+ await runWithMigrationBypassForIdentifier(userId, async () => {
172
+ await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = ${sqlPlaceholder(1)}`, [
173
+ userId
174
+ ]);
175
+ });
130
176
  }
131
177
  async destroyOtherSessions(userId, keepSessionId) {
132
- await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = ${sqlPlaceholder(1)} AND id <> ${sqlPlaceholder(2)}`, [userId, keepSessionId]);
178
+ await runWithMigrationBypassForIdentifier(userId, async () => {
179
+ await this.sql().unsafe(`DELETE FROM sessions WHERE user_id = ${sqlPlaceholder(1)} AND id <> ${sqlPlaceholder(2)}`, [userId, keepSessionId]);
180
+ });
133
181
  }
134
182
  async listForUser(userId) {
135
183
  const dialect = currentSqlDialect();
136
- return this.sql().unsafe(`SELECT id, user_id, user_agent, ip_address, last_active_at, expires_at
184
+ return await runWithMigrationBypassForIdentifier(userId, () => this.sql().unsafe(`SELECT id, user_id, user_agent, ip_address, last_active_at, expires_at
137
185
  FROM sessions
138
186
  WHERE user_id = ${dialect.placeholder(1)} AND expires_at > ${dialect.nowExpression()}
139
- ORDER BY last_active_at DESC${dialect.nullsLastSuffix()}, expires_at DESC`, [userId]);
187
+ ORDER BY last_active_at DESC${dialect.nullsLastSuffix()}, expires_at DESC`, [userId]));
140
188
  }
141
189
  async touch(sessionId) {
142
- await this.sql().unsafe(`UPDATE sessions SET last_active_at = ${sqlNow()} WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
190
+ await runWithMigrationBypassForIdentifier(sessionId, async () => {
191
+ await this.sql().unsafe(`UPDATE sessions SET last_active_at = ${sqlNow()} WHERE id = ${sqlPlaceholder(1)}`, [sessionId]);
192
+ });
143
193
  }
144
194
  async read(request) {
145
195
  const sessionId = this.sessionIdFromRequest(request);