fluxmail 0.4.1 → 0.6.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/README.md +6 -4
- package/dist/accounts/defaultGoogleOAuth.d.ts +4 -0
- package/dist/accounts/defaultGoogleOAuth.d.ts.map +1 -0
- package/dist/accounts/defaultGoogleOAuth.js +7 -0
- package/dist/accounts/defaultGoogleOAuth.js.map +1 -0
- package/dist/accounts/gmailConnection.js +2 -2
- package/dist/accounts/gmailConnection.js.map +1 -1
- package/dist/accounts/googleAuth.d.ts +16 -9
- package/dist/accounts/googleAuth.d.ts.map +1 -1
- package/dist/accounts/googleAuth.js +52 -20
- package/dist/accounts/googleAuth.js.map +1 -1
- package/dist/accounts/microsoftAuth.d.ts +5 -2
- package/dist/accounts/microsoftAuth.d.ts.map +1 -1
- package/dist/accounts/microsoftAuth.js +25 -22
- package/dist/accounts/microsoftAuth.js.map +1 -1
- package/dist/accounts/registry.d.ts +11 -9
- package/dist/accounts/registry.d.ts.map +1 -1
- package/dist/accounts/registry.js +169 -98
- package/dist/accounts/registry.js.map +1 -1
- package/dist/auth.d.ts +106 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +455 -0
- package/dist/auth.js.map +1 -0
- package/dist/authorization.d.ts +9 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +23 -0
- package/dist/authorization.js.map +1 -0
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1096 -403
- package/dist/cli.js.map +1 -1
- package/dist/cliInstances.d.ts +51 -0
- package/dist/cliInstances.d.ts.map +1 -0
- package/dist/cliInstances.js +237 -0
- package/dist/cliInstances.js.map +1 -0
- package/dist/cliMail.d.ts +9 -0
- package/dist/cliMail.d.ts.map +1 -0
- package/dist/cliMail.js +450 -0
- package/dist/cliMail.js.map +1 -0
- package/dist/config.d.ts +95 -118
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +351 -286
- package/dist/config.js.map +1 -1
- package/dist/configMigration.d.ts +8 -0
- package/dist/configMigration.d.ts.map +1 -0
- package/dist/configMigration.js +234 -0
- package/dist/configMigration.js.map +1 -0
- package/dist/context.d.ts +8 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +29 -8
- package/dist/context.js.map +1 -1
- package/dist/deploymentConfig.d.ts +190 -0
- package/dist/deploymentConfig.d.ts.map +1 -0
- package/dist/deploymentConfig.js +539 -0
- package/dist/deploymentConfig.js.map +1 -0
- package/dist/http/admin.d.ts +75 -3
- package/dist/http/admin.d.ts.map +1 -1
- package/dist/http/admin.js +347 -79
- package/dist/http/admin.js.map +1 -1
- package/dist/http/app.d.ts +4 -1
- package/dist/http/app.d.ts.map +1 -1
- package/dist/http/app.js +119 -63
- package/dist/http/app.js.map +1 -1
- package/dist/http/identity.d.ts +114 -0
- package/dist/http/identity.d.ts.map +1 -0
- package/dist/http/identity.js +936 -0
- package/dist/http/identity.js.map +1 -0
- package/dist/http/operationRoutes.d.ts +8 -0
- package/dist/http/operationRoutes.d.ts.map +1 -0
- package/dist/http/operationRoutes.js +13 -0
- package/dist/http/operationRoutes.js.map +1 -0
- package/dist/http/rest.d.ts +16 -4
- package/dist/http/rest.d.ts.map +1 -1
- package/dist/http/rest.js +213 -71
- package/dist/http/rest.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/instanceConfig.d.ts +36 -0
- package/dist/instanceConfig.d.ts.map +1 -0
- package/dist/instanceConfig.js +157 -0
- package/dist/instanceConfig.js.map +1 -0
- package/dist/licensing/activation.d.ts +2 -1
- package/dist/licensing/activation.d.ts.map +1 -1
- package/dist/licensing/activation.js +2 -3
- package/dist/licensing/activation.js.map +1 -1
- package/dist/licensing/entitlements.d.ts.map +1 -1
- package/dist/licensing/entitlements.js +2 -2
- package/dist/licensing/entitlements.js.map +1 -1
- package/dist/licensing/refresher.d.ts +6 -1
- package/dist/licensing/refresher.d.ts.map +1 -1
- package/dist/licensing/refresher.js +18 -7
- package/dist/licensing/refresher.js.map +1 -1
- package/dist/logging.d.ts +87 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +612 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp/buildServer.d.ts +3 -1
- package/dist/mcp/buildServer.d.ts.map +1 -1
- package/dist/mcp/buildServer.js +27 -24
- package/dist/mcp/buildServer.js.map +1 -1
- package/dist/permissions.d.ts +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +10 -4
- package/dist/permissions.js.map +1 -1
- package/dist/scheduler/sendScheduler.d.ts +4 -3
- package/dist/scheduler/sendScheduler.d.ts.map +1 -1
- package/dist/scheduler/sendScheduler.js +15 -3
- package/dist/scheduler/sendScheduler.js.map +1 -1
- package/dist/service/emailService.d.ts +10 -17
- package/dist/service/emailService.d.ts.map +1 -1
- package/dist/service/emailService.js +28 -38
- package/dist/service/emailService.js.map +1 -1
- package/dist/storage/adminAudit.d.ts +15 -2
- package/dist/storage/adminAudit.d.ts.map +1 -1
- package/dist/storage/adminAudit.js +22 -27
- package/dist/storage/adminAudit.js.map +1 -1
- package/dist/storage/apiKeys.d.ts +8 -8
- package/dist/storage/apiKeys.d.ts.map +1 -1
- package/dist/storage/apiKeys.js +16 -11
- package/dist/storage/apiKeys.js.map +1 -1
- package/dist/storage/db.d.ts +596 -31
- package/dist/storage/db.d.ts.map +1 -1
- package/dist/storage/db.js +383 -45
- package/dist/storage/db.js.map +1 -1
- package/dist/storage/fileLock.d.ts +7 -0
- package/dist/storage/fileLock.d.ts.map +1 -0
- package/dist/storage/fileLock.js +183 -0
- package/dist/storage/fileLock.js.map +1 -0
- package/dist/storage/gmailConnectionGrants.d.ts +3 -4
- package/dist/storage/gmailConnectionGrants.d.ts.map +1 -1
- package/dist/storage/gmailConnectionGrants.js +6 -6
- package/dist/storage/gmailConnectionGrants.js.map +1 -1
- package/dist/storage/members.d.ts +11 -0
- package/dist/storage/members.d.ts.map +1 -1
- package/dist/storage/members.js +70 -6
- package/dist/storage/members.js.map +1 -1
- package/dist/telemetry.d.ts +17 -2
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +34 -19
- package/dist/telemetry.js.map +1 -1
- package/dist/updateNotifier.d.ts +18 -0
- package/dist/updateNotifier.d.ts.map +1 -0
- package/dist/updateNotifier.js +128 -0
- package/dist/updateNotifier.js.map +1 -0
- package/package.json +38 -5
package/dist/storage/db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/storage/db.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,KAAK,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/storage/db.ts"],"names":[],"mappings":"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"}
|
package/dist/storage/db.js
CHANGED
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import Database from 'better-sqlite3';
|
|
2
2
|
import { drizzle } from 'drizzle-orm/better-sqlite3';
|
|
3
3
|
import { index, integer, primaryKey, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
|
4
|
+
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { VERSION } from '../version.js';
|
|
8
|
+
import { decryptString } from './crypto.js';
|
|
9
|
+
import { withFileLock } from './fileLock.js';
|
|
10
|
+
export const CURRENT_STORE_FORMAT = 2;
|
|
11
|
+
export const MIN_SUPPORTED_STORE_FORMAT = 1;
|
|
12
|
+
export const MAX_SUPPORTED_STORE_FORMAT = CURRENT_STORE_FORMAT;
|
|
13
|
+
export const LEGACY_STORE_FORMAT = 0;
|
|
14
|
+
const MIGRATION_LOCK_TIMEOUT_MS = 5 * 60_000;
|
|
15
|
+
const MIGRATION_LOCK_STALE_MS = 30 * 60_000;
|
|
16
|
+
export class IncompatibleStoreError extends Error {
|
|
17
|
+
compatibility;
|
|
18
|
+
code = 'incompatible_store';
|
|
19
|
+
constructor(compatibility) {
|
|
20
|
+
const supportedFormats = compatibility.minimumSupportedFormat === compatibility.maximumSupportedFormat
|
|
21
|
+
? `format ${compatibility.minimumSupportedFormat}`
|
|
22
|
+
: `formats ${compatibility.minimumSupportedFormat} through ${compatibility.maximumSupportedFormat}`;
|
|
23
|
+
const action = compatibility.storeFormat > compatibility.maximumSupportedFormat
|
|
24
|
+
? 'Update Fluxmail'
|
|
25
|
+
: 'Use a Fluxmail version that supports this store';
|
|
26
|
+
super(`This Fluxmail data uses store format ${compatibility.storeFormat}, but Fluxmail ${compatibility.engineVersion} supports ${supportedFormats}. ${action} before opening ${compatibility.dataDir}. Fluxmail did not change the store.`);
|
|
27
|
+
this.compatibility = compatibility;
|
|
28
|
+
this.name = 'IncompatibleStoreError';
|
|
29
|
+
}
|
|
30
|
+
}
|
|
4
31
|
export const members = sqliteTable('members', {
|
|
5
32
|
id: text('id').primaryKey(),
|
|
6
33
|
name: text('name').notNull(),
|
|
7
34
|
email: text('email'),
|
|
8
35
|
role: text('role').notNull().default('member'),
|
|
36
|
+
status: text('status').notNull().default('pending'),
|
|
9
37
|
createdAt: integer('created_at').notNull(),
|
|
10
38
|
}, (table) => [uniqueIndex('members_email_unique').on(table.email)]);
|
|
11
39
|
export const accounts = sqliteTable('accounts', {
|
|
@@ -15,11 +43,11 @@ export const accounts = sqliteTable('accounts', {
|
|
|
15
43
|
displayName: text('display_name'),
|
|
16
44
|
status: text('status').notNull().default('active'),
|
|
17
45
|
createdAt: integer('created_at').notNull(),
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
46
|
+
/** Transitional nulls only exist until a migrated installation claims its first administrator. */
|
|
47
|
+
ownerMemberId: text('member_id').references(() => members.id, { onDelete: 'restrict' }),
|
|
48
|
+
sharedWithAll: integer('shared_with_all', { mode: 'boolean' }).notNull().default(false),
|
|
21
49
|
}, (table) => [uniqueIndex('accounts_provider_email_unique').on(table.provider, table.email)]);
|
|
22
|
-
export const
|
|
50
|
+
export const accountMemberGrants = sqliteTable('account_member_grants', {
|
|
23
51
|
accountId: text('account_id')
|
|
24
52
|
.notNull()
|
|
25
53
|
.references(() => accounts.id, { onDelete: 'cascade' }),
|
|
@@ -42,6 +70,7 @@ export const accountCredentials = sqliteTable('account_credentials', {
|
|
|
42
70
|
/** AES-256-GCM encrypted provider-specific credentials. */
|
|
43
71
|
encryptedCredentials: text('encrypted_credentials').notNull(),
|
|
44
72
|
updatedAt: integer('updated_at').notNull(),
|
|
73
|
+
revision: integer('revision').notNull().default(1),
|
|
45
74
|
});
|
|
46
75
|
export const imapMessages = sqliteTable('imap_messages', {
|
|
47
76
|
id: text('id').primaryKey(),
|
|
@@ -72,8 +101,10 @@ export const apiKeys = sqliteTable('api_keys', {
|
|
|
72
101
|
keyHash: text('key_hash').notNull().unique(),
|
|
73
102
|
createdAt: integer('created_at').notNull(),
|
|
74
103
|
lastUsedAt: integer('last_used_at'),
|
|
75
|
-
/** Member the key was issued to
|
|
76
|
-
memberId: text('member_id')
|
|
104
|
+
/** Member the key was issued to. */
|
|
105
|
+
memberId: text('member_id')
|
|
106
|
+
.notNull()
|
|
107
|
+
.references(() => members.id, { onDelete: 'cascade' }),
|
|
77
108
|
/** Named MCP permission profile, or "custom" when customCapabilities is populated. */
|
|
78
109
|
permissionProfile: text('permission_profile').notNull().default('full'),
|
|
79
110
|
/** JSON array of explicit MCP capabilities for custom policies. */
|
|
@@ -83,12 +114,57 @@ export const apiKeys = sqliteTable('api_keys', {
|
|
|
83
114
|
/** NULL means no extra narrowing; otherwise a JSON array of canonical account ids. */
|
|
84
115
|
accountIds: text('account_ids'),
|
|
85
116
|
});
|
|
117
|
+
export const memberCredentials = sqliteTable('member_credentials', {
|
|
118
|
+
memberId: text('member_id')
|
|
119
|
+
.primaryKey()
|
|
120
|
+
.references(() => members.id, { onDelete: 'cascade' }),
|
|
121
|
+
passwordHash: text('password_hash').notNull(),
|
|
122
|
+
passwordVersion: integer('password_version').notNull().default(1),
|
|
123
|
+
updatedAt: integer('updated_at').notNull(),
|
|
124
|
+
});
|
|
125
|
+
export const memberSessions = sqliteTable('member_sessions', {
|
|
126
|
+
id: text('id').primaryKey(),
|
|
127
|
+
memberId: text('member_id')
|
|
128
|
+
.notNull()
|
|
129
|
+
.references(() => members.id, { onDelete: 'cascade' }),
|
|
130
|
+
tokenHash: text('token_hash').notNull().unique(),
|
|
131
|
+
deviceName: text('device_name').notNull(),
|
|
132
|
+
createdAt: integer('created_at').notNull(),
|
|
133
|
+
expiresAt: integer('expires_at').notNull(),
|
|
134
|
+
lastUsedAt: integer('last_used_at').notNull(),
|
|
135
|
+
revokedAt: integer('revoked_at'),
|
|
136
|
+
}, (table) => [index('member_sessions_member').on(table.memberId), index('member_sessions_expiry').on(table.expiresAt)]);
|
|
137
|
+
export const memberAuthTokens = sqliteTable('member_auth_tokens', {
|
|
138
|
+
id: text('id').primaryKey(),
|
|
139
|
+
memberId: text('member_id')
|
|
140
|
+
.notNull()
|
|
141
|
+
.references(() => members.id, { onDelete: 'cascade' }),
|
|
142
|
+
kind: text('kind').notNull(),
|
|
143
|
+
tokenHash: text('token_hash').notNull().unique(),
|
|
144
|
+
createdByMemberId: text('created_by_member_id').references(() => members.id, { onDelete: 'set null' }),
|
|
145
|
+
createdAt: integer('created_at').notNull(),
|
|
146
|
+
expiresAt: integer('expires_at').notNull(),
|
|
147
|
+
usedAt: integer('used_at'),
|
|
148
|
+
}, (table) => [
|
|
149
|
+
index('member_auth_tokens_member').on(table.memberId),
|
|
150
|
+
index('member_auth_tokens_expiry').on(table.expiresAt),
|
|
151
|
+
]);
|
|
152
|
+
export const authRateLimits = sqliteTable('auth_rate_limits', {
|
|
153
|
+
key: text('key').primaryKey(),
|
|
154
|
+
attempts: integer('attempts').notNull(),
|
|
155
|
+
windowStartedAt: integer('window_started_at').notNull(),
|
|
156
|
+
}, (table) => [index('auth_rate_limits_window').on(table.windowStartedAt)]);
|
|
157
|
+
export const instanceSettings = sqliteTable('instance_settings', {
|
|
158
|
+
key: text('key').primaryKey(),
|
|
159
|
+
value: text('value').notNull(),
|
|
160
|
+
});
|
|
86
161
|
export const adminAuditEvents = sqliteTable('admin_audit_events', {
|
|
87
162
|
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
88
163
|
timestamp: integer('timestamp').notNull(),
|
|
89
164
|
operation: text('operation').notNull(),
|
|
90
165
|
outcome: text('outcome').notNull(),
|
|
91
166
|
actorKeyId: text('actor_key_id'),
|
|
167
|
+
actorSessionId: text('actor_session_id'),
|
|
92
168
|
actorMemberId: text('actor_member_id'),
|
|
93
169
|
resourceType: text('resource_type'),
|
|
94
170
|
resourceId: text('resource_id'),
|
|
@@ -98,10 +174,10 @@ export const gmailConnectionGrants = sqliteTable('gmail_connection_grants', {
|
|
|
98
174
|
/** SHA-256 hex digest. The raw token is printed once and never stored. */
|
|
99
175
|
tokenHash: text('token_hash').primaryKey(),
|
|
100
176
|
scope: text('scope').notNull(),
|
|
101
|
-
|
|
177
|
+
ownerMemberId: text('owner_member_id'),
|
|
102
178
|
reauthorizeAccountId: text('reauthorize_account_id'),
|
|
103
|
-
|
|
104
|
-
|
|
179
|
+
sharedWithAll: integer('shared_with_all', { mode: 'boolean' }),
|
|
180
|
+
grantedMemberIds: text('granted_member_ids'),
|
|
105
181
|
createdAt: integer('created_at').notNull(),
|
|
106
182
|
expiresAt: integer('expires_at').notNull(),
|
|
107
183
|
consumedAt: integer('consumed_at'),
|
|
@@ -152,6 +228,7 @@ CREATE TABLE IF NOT EXISTS members (
|
|
|
152
228
|
name TEXT NOT NULL,
|
|
153
229
|
email TEXT,
|
|
154
230
|
role TEXT NOT NULL DEFAULT 'member',
|
|
231
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
155
232
|
created_at INTEGER NOT NULL
|
|
156
233
|
);
|
|
157
234
|
CREATE UNIQUE INDEX IF NOT EXISTS members_email_unique ON members(email);
|
|
@@ -162,10 +239,10 @@ CREATE TABLE IF NOT EXISTS accounts (
|
|
|
162
239
|
display_name TEXT,
|
|
163
240
|
status TEXT NOT NULL DEFAULT 'active',
|
|
164
241
|
created_at INTEGER NOT NULL,
|
|
165
|
-
member_id TEXT REFERENCES members(id) ON DELETE
|
|
166
|
-
|
|
242
|
+
member_id TEXT REFERENCES members(id) ON DELETE RESTRICT,
|
|
243
|
+
shared_with_all INTEGER NOT NULL DEFAULT 0
|
|
167
244
|
);
|
|
168
|
-
CREATE TABLE IF NOT EXISTS
|
|
245
|
+
CREATE TABLE IF NOT EXISTS account_member_grants (
|
|
169
246
|
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
|
170
247
|
member_id TEXT NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
|
171
248
|
PRIMARY KEY (account_id, member_id)
|
|
@@ -178,7 +255,8 @@ CREATE TABLE IF NOT EXISTS oauth_tokens (
|
|
|
178
255
|
CREATE TABLE IF NOT EXISTS account_credentials (
|
|
179
256
|
account_id TEXT PRIMARY KEY REFERENCES accounts(id) ON DELETE CASCADE,
|
|
180
257
|
encrypted_credentials TEXT NOT NULL,
|
|
181
|
-
updated_at INTEGER NOT NULL
|
|
258
|
+
updated_at INTEGER NOT NULL,
|
|
259
|
+
revision INTEGER NOT NULL DEFAULT 1
|
|
182
260
|
);
|
|
183
261
|
CREATE TABLE IF NOT EXISTS imap_messages (
|
|
184
262
|
id TEXT PRIMARY KEY,
|
|
@@ -209,18 +287,59 @@ CREATE TABLE IF NOT EXISTS api_keys (
|
|
|
209
287
|
key_hash TEXT NOT NULL UNIQUE,
|
|
210
288
|
created_at INTEGER NOT NULL,
|
|
211
289
|
last_used_at INTEGER,
|
|
212
|
-
member_id TEXT REFERENCES members(id) ON DELETE
|
|
290
|
+
member_id TEXT NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
|
213
291
|
permission_profile TEXT NOT NULL DEFAULT 'full',
|
|
214
292
|
custom_capabilities TEXT,
|
|
215
293
|
supplemental_capabilities TEXT NOT NULL DEFAULT '[]',
|
|
216
294
|
account_ids TEXT
|
|
217
295
|
);
|
|
296
|
+
CREATE TABLE IF NOT EXISTS member_credentials (
|
|
297
|
+
member_id TEXT PRIMARY KEY REFERENCES members(id) ON DELETE CASCADE,
|
|
298
|
+
password_hash TEXT NOT NULL,
|
|
299
|
+
password_version INTEGER NOT NULL DEFAULT 1,
|
|
300
|
+
updated_at INTEGER NOT NULL
|
|
301
|
+
);
|
|
302
|
+
CREATE TABLE IF NOT EXISTS member_sessions (
|
|
303
|
+
id TEXT PRIMARY KEY,
|
|
304
|
+
member_id TEXT NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
|
305
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
306
|
+
device_name TEXT NOT NULL,
|
|
307
|
+
created_at INTEGER NOT NULL,
|
|
308
|
+
expires_at INTEGER NOT NULL,
|
|
309
|
+
last_used_at INTEGER NOT NULL,
|
|
310
|
+
revoked_at INTEGER
|
|
311
|
+
);
|
|
312
|
+
CREATE INDEX IF NOT EXISTS member_sessions_member ON member_sessions(member_id);
|
|
313
|
+
CREATE INDEX IF NOT EXISTS member_sessions_expiry ON member_sessions(expires_at);
|
|
314
|
+
CREATE TABLE IF NOT EXISTS member_auth_tokens (
|
|
315
|
+
id TEXT PRIMARY KEY,
|
|
316
|
+
member_id TEXT NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
|
317
|
+
kind TEXT NOT NULL,
|
|
318
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
319
|
+
created_by_member_id TEXT REFERENCES members(id) ON DELETE SET NULL,
|
|
320
|
+
created_at INTEGER NOT NULL,
|
|
321
|
+
expires_at INTEGER NOT NULL,
|
|
322
|
+
used_at INTEGER
|
|
323
|
+
);
|
|
324
|
+
CREATE INDEX IF NOT EXISTS member_auth_tokens_member ON member_auth_tokens(member_id);
|
|
325
|
+
CREATE INDEX IF NOT EXISTS member_auth_tokens_expiry ON member_auth_tokens(expires_at);
|
|
326
|
+
CREATE TABLE IF NOT EXISTS auth_rate_limits (
|
|
327
|
+
key TEXT PRIMARY KEY,
|
|
328
|
+
attempts INTEGER NOT NULL,
|
|
329
|
+
window_started_at INTEGER NOT NULL
|
|
330
|
+
);
|
|
331
|
+
CREATE INDEX IF NOT EXISTS auth_rate_limits_window ON auth_rate_limits(window_started_at);
|
|
332
|
+
CREATE TABLE IF NOT EXISTS instance_settings (
|
|
333
|
+
key TEXT PRIMARY KEY,
|
|
334
|
+
value TEXT NOT NULL
|
|
335
|
+
);
|
|
218
336
|
CREATE TABLE IF NOT EXISTS admin_audit_events (
|
|
219
337
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
220
338
|
timestamp INTEGER NOT NULL,
|
|
221
339
|
operation TEXT NOT NULL,
|
|
222
340
|
outcome TEXT NOT NULL,
|
|
223
341
|
actor_key_id TEXT,
|
|
342
|
+
actor_session_id TEXT,
|
|
224
343
|
actor_member_id TEXT,
|
|
225
344
|
resource_type TEXT,
|
|
226
345
|
resource_id TEXT,
|
|
@@ -228,13 +347,23 @@ CREATE TABLE IF NOT EXISTS admin_audit_events (
|
|
|
228
347
|
);
|
|
229
348
|
CREATE INDEX IF NOT EXISTS admin_audit_events_timestamp
|
|
230
349
|
ON admin_audit_events(timestamp);
|
|
350
|
+
CREATE TRIGGER IF NOT EXISTS admin_audit_events_no_update
|
|
351
|
+
BEFORE UPDATE ON admin_audit_events
|
|
352
|
+
BEGIN
|
|
353
|
+
SELECT RAISE(ABORT, 'security audit events are append-only');
|
|
354
|
+
END;
|
|
355
|
+
CREATE TRIGGER IF NOT EXISTS admin_audit_events_no_delete
|
|
356
|
+
BEFORE DELETE ON admin_audit_events
|
|
357
|
+
BEGIN
|
|
358
|
+
SELECT RAISE(ABORT, 'security audit events are append-only');
|
|
359
|
+
END;
|
|
231
360
|
CREATE TABLE IF NOT EXISTS gmail_connection_grants (
|
|
232
361
|
token_hash TEXT PRIMARY KEY,
|
|
233
362
|
scope TEXT NOT NULL,
|
|
234
|
-
|
|
363
|
+
owner_member_id TEXT,
|
|
235
364
|
reauthorize_account_id TEXT,
|
|
236
|
-
|
|
237
|
-
|
|
365
|
+
shared_with_all INTEGER,
|
|
366
|
+
granted_member_ids TEXT,
|
|
238
367
|
created_at INTEGER NOT NULL,
|
|
239
368
|
expires_at INTEGER NOT NULL,
|
|
240
369
|
consumed_at INTEGER
|
|
@@ -281,21 +410,108 @@ CREATE INDEX IF NOT EXISTS rest_idempotency_expires_at
|
|
|
281
410
|
function tableColumns(sqlite, table) {
|
|
282
411
|
return new Set(sqlite.prepare(`PRAGMA table_info(${table})`).all().map((column) => column.name));
|
|
283
412
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
sqlite.
|
|
413
|
+
function readStoreFormat(sqlite) {
|
|
414
|
+
return sqlite.pragma('user_version', { simple: true });
|
|
415
|
+
}
|
|
416
|
+
function sqliteTableExists(sqlite, table) {
|
|
417
|
+
return Boolean(sqlite.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table));
|
|
418
|
+
}
|
|
419
|
+
export function inspectStoreEncryptionKey(dbPath, encryptionKey) {
|
|
420
|
+
if (dbPath === ':memory:' || !existsSync(dbPath) || statSync(dbPath).size === 0)
|
|
421
|
+
return 'empty';
|
|
422
|
+
const sqlite = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
423
|
+
try {
|
|
424
|
+
const encryptedValues = [];
|
|
425
|
+
if (sqliteTableExists(sqlite, 'account_credentials')) {
|
|
426
|
+
encryptedValues.push(...sqlite.prepare('SELECT encrypted_credentials AS value FROM account_credentials').all().map((row) => row.value));
|
|
427
|
+
}
|
|
428
|
+
if (sqliteTableExists(sqlite, 'oauth_tokens')) {
|
|
429
|
+
encryptedValues.push(...sqlite.prepare('SELECT encrypted_tokens AS value FROM oauth_tokens').all().map((row) => row.value));
|
|
430
|
+
}
|
|
431
|
+
if (sqliteTableExists(sqlite, 'instance_settings')) {
|
|
432
|
+
encryptedValues.push(...sqlite.prepare("SELECT value FROM instance_settings WHERE value LIKE 'v1:%'").all().map((row) => row.value.slice('v1:'.length)));
|
|
433
|
+
}
|
|
434
|
+
if (!encryptedValues.length)
|
|
435
|
+
return 'empty';
|
|
436
|
+
try {
|
|
437
|
+
for (const value of encryptedValues)
|
|
438
|
+
decryptString(encryptionKey, value);
|
|
439
|
+
return 'matches';
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
return 'mismatch';
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
finally {
|
|
446
|
+
sqlite.close();
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function compatibilityFor(dbPath, dataDir, storeFormat) {
|
|
450
|
+
return {
|
|
451
|
+
engineVersion: VERSION,
|
|
452
|
+
dataDir,
|
|
453
|
+
dbPath,
|
|
454
|
+
storeFormat,
|
|
455
|
+
minimumSupportedFormat: MIN_SUPPORTED_STORE_FORMAT,
|
|
456
|
+
maximumSupportedFormat: MAX_SUPPORTED_STORE_FORMAT,
|
|
457
|
+
compatible: storeFormat === LEGACY_STORE_FORMAT ||
|
|
458
|
+
(storeFormat >= MIN_SUPPORTED_STORE_FORMAT && storeFormat <= MAX_SUPPORTED_STORE_FORMAT),
|
|
459
|
+
requiresMigration: storeFormat === LEGACY_STORE_FORMAT || storeFormat < CURRENT_STORE_FORMAT,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
export function inspectStoreCompatibility(dbPath, dataDir = dbPath === ':memory:' ? ':memory:' : path.dirname(dbPath)) {
|
|
463
|
+
if (dbPath === ':memory:' || !existsSync(dbPath) || statSync(dbPath).size === 0) {
|
|
464
|
+
return compatibilityFor(dbPath, dataDir, LEGACY_STORE_FORMAT);
|
|
465
|
+
}
|
|
466
|
+
const sqlite = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
467
|
+
try {
|
|
468
|
+
return compatibilityFor(dbPath, dataDir, readStoreFormat(sqlite));
|
|
469
|
+
}
|
|
470
|
+
finally {
|
|
471
|
+
sqlite.close();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
function assertCompatible(compatibility) {
|
|
475
|
+
if (!compatibility.compatible)
|
|
476
|
+
throw new IncompatibleStoreError(compatibility);
|
|
477
|
+
}
|
|
478
|
+
function backupForMigration(sqlite, dbPath, fromFormat, toFormat) {
|
|
479
|
+
if (dbPath === ':memory:' || !existsSync(dbPath) || statSync(dbPath).size === 0)
|
|
480
|
+
return undefined;
|
|
481
|
+
const backupDir = path.join(path.dirname(dbPath), 'backups');
|
|
482
|
+
mkdirSync(backupDir, { recursive: true });
|
|
483
|
+
const stamp = new Date().toISOString().replaceAll(/[:.]/g, '-');
|
|
484
|
+
const databaseName = path.basename(dbPath, path.extname(dbPath)) || 'fluxmail';
|
|
485
|
+
const suffix = randomBytes(4).toString('hex');
|
|
486
|
+
const destination = path.join(backupDir, `${databaseName}-format-${fromFormat}-to-${toFormat}-${stamp}-${process.pid}-${suffix}.db`);
|
|
487
|
+
sqlite.exec(`VACUUM INTO '${destination.replaceAll("'", "''")}'`);
|
|
488
|
+
return destination;
|
|
489
|
+
}
|
|
490
|
+
function syncLegacyCredentialRows(sqlite) {
|
|
289
491
|
// Keep the generic credential row in sync if an older binary updated the
|
|
290
492
|
// legacy Gmail token table before this version reopened the database.
|
|
291
493
|
sqlite.exec(`
|
|
292
|
-
INSERT INTO account_credentials (account_id, encrypted_credentials, updated_at)
|
|
293
|
-
SELECT account_id, encrypted_tokens, updated_at FROM oauth_tokens WHERE true
|
|
494
|
+
INSERT INTO account_credentials (account_id, encrypted_credentials, updated_at, revision)
|
|
495
|
+
SELECT account_id, encrypted_tokens, updated_at, 1 FROM oauth_tokens WHERE true
|
|
294
496
|
ON CONFLICT(account_id) DO UPDATE SET
|
|
295
497
|
encrypted_credentials = excluded.encrypted_credentials,
|
|
296
|
-
updated_at = excluded.updated_at
|
|
498
|
+
updated_at = excluded.updated_at,
|
|
499
|
+
revision = account_credentials.revision + 1
|
|
297
500
|
WHERE excluded.updated_at > account_credentials.updated_at
|
|
298
501
|
`);
|
|
502
|
+
}
|
|
503
|
+
function migrateToFormatOne(sqlite) {
|
|
504
|
+
const existingMemberColumns = tableColumns(sqlite, 'members');
|
|
505
|
+
const existingApiKeyColumns = tableColumns(sqlite, 'api_keys');
|
|
506
|
+
const existingAccountColumns = tableColumns(sqlite, 'accounts');
|
|
507
|
+
const hadLegacyAccountShares = tableColumns(sqlite, 'account_member_shares').size > 0;
|
|
508
|
+
const legacyAuthentication = (existingMemberColumns.size > 0 && !existingMemberColumns.has('status')) ||
|
|
509
|
+
(existingMemberColumns.size === 0 && existingApiKeyColumns.size > 0);
|
|
510
|
+
sqlite.exec(BOOTSTRAP_SQL);
|
|
511
|
+
const credentialCols = tableColumns(sqlite, 'account_credentials');
|
|
512
|
+
if (!credentialCols.has('revision')) {
|
|
513
|
+
sqlite.exec('ALTER TABLE account_credentials ADD COLUMN revision INTEGER NOT NULL DEFAULT 1');
|
|
514
|
+
}
|
|
299
515
|
const scheduledCols = tableColumns(sqlite, 'scheduled_sends');
|
|
300
516
|
if (!scheduledCols.has('claim_token'))
|
|
301
517
|
sqlite.exec('ALTER TABLE scheduled_sends ADD COLUMN claim_token TEXT');
|
|
@@ -310,29 +526,35 @@ export function openDb(dbPath) {
|
|
|
310
526
|
WHERE id = (SELECT id FROM members ORDER BY created_at, id LIMIT 1)
|
|
311
527
|
`);
|
|
312
528
|
}
|
|
529
|
+
if (!memberCols.has('status')) {
|
|
530
|
+
sqlite.exec("ALTER TABLE members ADD COLUMN status TEXT NOT NULL DEFAULT 'pending'");
|
|
531
|
+
}
|
|
313
532
|
if (!tableColumns(sqlite, 'accounts').has('member_id')) {
|
|
314
533
|
sqlite.exec('ALTER TABLE accounts ADD COLUMN member_id TEXT REFERENCES members(id) ON DELETE SET NULL');
|
|
315
534
|
}
|
|
316
535
|
const accountCols = tableColumns(sqlite, 'accounts');
|
|
317
|
-
if (!accountCols.has('
|
|
318
|
-
sqlite.exec('ALTER TABLE accounts ADD COLUMN
|
|
319
|
-
sqlite.exec(
|
|
536
|
+
if (!accountCols.has('shared_with_all')) {
|
|
537
|
+
sqlite.exec('ALTER TABLE accounts ADD COLUMN shared_with_all INTEGER NOT NULL DEFAULT 0');
|
|
538
|
+
sqlite.exec(existingAccountColumns.has('sharing_mode')
|
|
539
|
+
? "UPDATE accounts SET shared_with_all = CASE WHEN sharing_mode = 'all' THEN 1 ELSE 0 END"
|
|
540
|
+
: 'UPDATE accounts SET shared_with_all = CASE WHEN member_id IS NULL THEN 1 ELSE 0 END');
|
|
320
541
|
}
|
|
321
542
|
sqlite.exec(`
|
|
322
|
-
CREATE TABLE IF NOT EXISTS
|
|
543
|
+
CREATE TABLE IF NOT EXISTS account_member_grants (
|
|
323
544
|
account_id TEXT NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
|
324
545
|
member_id TEXT NOT NULL REFERENCES members(id) ON DELETE CASCADE,
|
|
325
546
|
PRIMARY KEY (account_id, member_id)
|
|
326
547
|
)
|
|
327
548
|
`);
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
549
|
+
if (hadLegacyAccountShares) {
|
|
550
|
+
sqlite.exec(`
|
|
551
|
+
INSERT OR IGNORE INTO account_member_grants (account_id, member_id)
|
|
552
|
+
SELECT account_id, member_id FROM account_member_shares
|
|
553
|
+
`);
|
|
554
|
+
sqlite.exec('DROP TABLE account_member_shares');
|
|
555
|
+
}
|
|
556
|
+
if (accountCols.has('sharing_mode'))
|
|
557
|
+
sqlite.exec('ALTER TABLE accounts DROP COLUMN sharing_mode');
|
|
336
558
|
if (!tableColumns(sqlite, 'api_keys').has('member_id')) {
|
|
337
559
|
sqlite.exec('ALTER TABLE api_keys ADD COLUMN member_id TEXT REFERENCES members(id) ON DELETE SET NULL');
|
|
338
560
|
}
|
|
@@ -345,9 +567,10 @@ export function openDb(dbPath) {
|
|
|
345
567
|
}
|
|
346
568
|
if (!apiKeyCols.has('supplemental_capabilities')) {
|
|
347
569
|
sqlite.exec("ALTER TABLE api_keys ADD COLUMN supplemental_capabilities TEXT NOT NULL DEFAULT '[]'");
|
|
348
|
-
// Preserve the account-management authority
|
|
349
|
-
//
|
|
350
|
-
// must still be granted explicitly.
|
|
570
|
+
// Preserve the account-management authority held by legacy administrator
|
|
571
|
+
// keys before administrative capabilities existed. Broader capabilities
|
|
572
|
+
// must still be granted explicitly. The breaking auth migration below
|
|
573
|
+
// revokes these keys before authenticated service access resumes.
|
|
351
574
|
sqlite.exec(`
|
|
352
575
|
UPDATE api_keys
|
|
353
576
|
SET supplemental_capabilities = '["admin.accounts"]'
|
|
@@ -373,13 +596,55 @@ export function openDb(dbPath) {
|
|
|
373
596
|
if (!apiKeyCols.has('account_ids')) {
|
|
374
597
|
sqlite.exec('ALTER TABLE api_keys ADD COLUMN account_ids TEXT');
|
|
375
598
|
}
|
|
599
|
+
if (legacyAuthentication)
|
|
600
|
+
sqlite.exec('DELETE FROM api_keys');
|
|
601
|
+
if (!tableColumns(sqlite, 'admin_audit_events').has('actor_session_id')) {
|
|
602
|
+
sqlite.exec('ALTER TABLE admin_audit_events ADD COLUMN actor_session_id TEXT');
|
|
603
|
+
}
|
|
376
604
|
const grantCols = tableColumns(sqlite, 'gmail_connection_grants');
|
|
377
|
-
if (!grantCols.has('
|
|
378
|
-
sqlite.exec('ALTER TABLE gmail_connection_grants ADD COLUMN
|
|
605
|
+
if (!grantCols.has('owner_member_id')) {
|
|
606
|
+
sqlite.exec('ALTER TABLE gmail_connection_grants ADD COLUMN owner_member_id TEXT');
|
|
607
|
+
if (grantCols.has('member_id')) {
|
|
608
|
+
sqlite.exec('UPDATE gmail_connection_grants SET owner_member_id = member_id');
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
if (!grantCols.has('shared_with_all')) {
|
|
612
|
+
sqlite.exec('ALTER TABLE gmail_connection_grants ADD COLUMN shared_with_all INTEGER');
|
|
613
|
+
if (grantCols.has('sharing_mode')) {
|
|
614
|
+
sqlite.exec("UPDATE gmail_connection_grants SET shared_with_all = CASE sharing_mode WHEN 'all' THEN 1 WHEN 'explicit' THEN 0 ELSE NULL END");
|
|
615
|
+
}
|
|
379
616
|
}
|
|
380
|
-
if (!grantCols.has('
|
|
381
|
-
sqlite.exec('ALTER TABLE gmail_connection_grants ADD COLUMN
|
|
617
|
+
if (!grantCols.has('granted_member_ids')) {
|
|
618
|
+
sqlite.exec('ALTER TABLE gmail_connection_grants ADD COLUMN granted_member_ids TEXT');
|
|
619
|
+
if (grantCols.has('shared_member_ids')) {
|
|
620
|
+
sqlite.exec('UPDATE gmail_connection_grants SET granted_member_ids = shared_member_ids');
|
|
621
|
+
}
|
|
382
622
|
}
|
|
623
|
+
if (grantCols.has('member_id'))
|
|
624
|
+
sqlite.exec('ALTER TABLE gmail_connection_grants DROP COLUMN member_id');
|
|
625
|
+
if (grantCols.has('sharing_mode'))
|
|
626
|
+
sqlite.exec('ALTER TABLE gmail_connection_grants DROP COLUMN sharing_mode');
|
|
627
|
+
if (grantCols.has('shared_member_ids')) {
|
|
628
|
+
sqlite.exec('ALTER TABLE gmail_connection_grants DROP COLUMN shared_member_ids');
|
|
629
|
+
}
|
|
630
|
+
// Login emails are case-insensitive. Keep the earliest duplicate address
|
|
631
|
+
// and require later legacy records to receive a new email before enrollment.
|
|
632
|
+
sqlite.exec(`
|
|
633
|
+
UPDATE members
|
|
634
|
+
SET email = NULL
|
|
635
|
+
WHERE email IS NOT NULL
|
|
636
|
+
AND EXISTS (
|
|
637
|
+
SELECT 1 FROM members AS winner
|
|
638
|
+
WHERE winner.email IS NOT NULL
|
|
639
|
+
AND lower(winner.email) = lower(members.email)
|
|
640
|
+
AND (
|
|
641
|
+
winner.created_at < members.created_at
|
|
642
|
+
OR (winner.created_at = members.created_at AND winner.id < members.id)
|
|
643
|
+
)
|
|
644
|
+
)
|
|
645
|
+
`);
|
|
646
|
+
sqlite.exec('DROP INDEX IF EXISTS members_email_unique');
|
|
647
|
+
sqlite.exec('CREATE UNIQUE INDEX IF NOT EXISTS members_email_unique_ci ON members(lower(email))');
|
|
383
648
|
// Account identity is the mailbox address, regardless of provider. Gmail
|
|
384
649
|
// wins when an older database contains the same address as both Gmail and IMAP.
|
|
385
650
|
sqlite.exec(`
|
|
@@ -403,6 +668,79 @@ export function openDb(dbPath) {
|
|
|
403
668
|
`);
|
|
404
669
|
sqlite.exec('DROP INDEX IF EXISTS accounts_provider_email_unique');
|
|
405
670
|
sqlite.exec('CREATE UNIQUE INDEX IF NOT EXISTS accounts_email_unique_ci ON accounts(lower(email))');
|
|
406
|
-
|
|
671
|
+
sqlite.exec(`
|
|
672
|
+
INSERT INTO instance_settings (key, value) VALUES ('schema_version', '2')
|
|
673
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
674
|
+
`);
|
|
675
|
+
sqlite.exec(`
|
|
676
|
+
INSERT OR IGNORE INTO instance_settings (key, value) VALUES ('bootstrap_complete', '0')
|
|
677
|
+
`);
|
|
678
|
+
}
|
|
679
|
+
function migrateToFormatTwo(_sqlite) {
|
|
680
|
+
// Format 2 reserves encrypted instance settings for OAuth applications and
|
|
681
|
+
// license state. The instance_settings table was created by format 1.
|
|
682
|
+
}
|
|
683
|
+
const MIGRATIONS = [
|
|
684
|
+
{ format: 1, run: migrateToFormatOne },
|
|
685
|
+
{ format: 2, run: migrateToFormatTwo },
|
|
686
|
+
];
|
|
687
|
+
function openCompatibleDb(dbPath, dataDir, options) {
|
|
688
|
+
const sqlite = new Database(dbPath);
|
|
689
|
+
try {
|
|
690
|
+
sqlite.pragma('busy_timeout = 5000');
|
|
691
|
+
sqlite.pragma('foreign_keys = ON');
|
|
692
|
+
const beforeMigration = compatibilityFor(dbPath, dataDir, readStoreFormat(sqlite));
|
|
693
|
+
assertCompatible(beforeMigration);
|
|
694
|
+
if (beforeMigration.requiresMigration && options.backupBeforeMigration !== false) {
|
|
695
|
+
backupForMigration(sqlite, dbPath, beforeMigration.storeFormat, CURRENT_STORE_FORMAT);
|
|
696
|
+
}
|
|
697
|
+
const migrate = sqlite.transaction(() => {
|
|
698
|
+
let storeFormat = readStoreFormat(sqlite);
|
|
699
|
+
assertCompatible(compatibilityFor(dbPath, dataDir, storeFormat));
|
|
700
|
+
for (const migration of MIGRATIONS) {
|
|
701
|
+
if (storeFormat >= migration.format)
|
|
702
|
+
continue;
|
|
703
|
+
migration.run(sqlite);
|
|
704
|
+
sqlite.pragma(`user_version = ${migration.format}`);
|
|
705
|
+
storeFormat = migration.format;
|
|
706
|
+
}
|
|
707
|
+
if (storeFormat !== CURRENT_STORE_FORMAT) {
|
|
708
|
+
throw new Error(`Fluxmail has no migration from store format ${storeFormat} to ${CURRENT_STORE_FORMAT}`);
|
|
709
|
+
}
|
|
710
|
+
syncLegacyCredentialRows(sqlite);
|
|
711
|
+
});
|
|
712
|
+
migrate.immediate();
|
|
713
|
+
sqlite.pragma('journal_mode = WAL');
|
|
714
|
+
return drizzle(sqlite);
|
|
715
|
+
}
|
|
716
|
+
catch (error) {
|
|
717
|
+
sqlite.close();
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
export function openDb(dbPath, options = {}) {
|
|
722
|
+
const dataDir = options.dataDir ?? (dbPath === ':memory:' ? ':memory:' : path.dirname(dbPath));
|
|
723
|
+
const inspected = inspectStoreCompatibility(dbPath, dataDir);
|
|
724
|
+
assertCompatible(inspected);
|
|
725
|
+
if (dbPath === ':memory:' || !inspected.requiresMigration) {
|
|
726
|
+
return openCompatibleDb(dbPath, dataDir, options);
|
|
727
|
+
}
|
|
728
|
+
return withFileLock(`${dbPath}.migration.lock`, {
|
|
729
|
+
timeoutMs: MIGRATION_LOCK_TIMEOUT_MS,
|
|
730
|
+
staleMs: MIGRATION_LOCK_STALE_MS,
|
|
731
|
+
description: `migration of ${dbPath}`,
|
|
732
|
+
}, () => openCompatibleDb(dbPath, dataDir, options));
|
|
733
|
+
}
|
|
734
|
+
export function openReadonlyDb(dbPath) {
|
|
735
|
+
const sqlite = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
736
|
+
try {
|
|
737
|
+
sqlite.pragma('query_only = ON');
|
|
738
|
+
sqlite.pragma('foreign_keys = ON');
|
|
739
|
+
return drizzle(sqlite);
|
|
740
|
+
}
|
|
741
|
+
catch (error) {
|
|
742
|
+
sqlite.close();
|
|
743
|
+
throw error;
|
|
744
|
+
}
|
|
407
745
|
}
|
|
408
746
|
//# sourceMappingURL=db.js.map
|