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.
Files changed (147) hide show
  1. package/README.md +6 -4
  2. package/dist/accounts/defaultGoogleOAuth.d.ts +4 -0
  3. package/dist/accounts/defaultGoogleOAuth.d.ts.map +1 -0
  4. package/dist/accounts/defaultGoogleOAuth.js +7 -0
  5. package/dist/accounts/defaultGoogleOAuth.js.map +1 -0
  6. package/dist/accounts/gmailConnection.js +2 -2
  7. package/dist/accounts/gmailConnection.js.map +1 -1
  8. package/dist/accounts/googleAuth.d.ts +16 -9
  9. package/dist/accounts/googleAuth.d.ts.map +1 -1
  10. package/dist/accounts/googleAuth.js +52 -20
  11. package/dist/accounts/googleAuth.js.map +1 -1
  12. package/dist/accounts/microsoftAuth.d.ts +5 -2
  13. package/dist/accounts/microsoftAuth.d.ts.map +1 -1
  14. package/dist/accounts/microsoftAuth.js +25 -22
  15. package/dist/accounts/microsoftAuth.js.map +1 -1
  16. package/dist/accounts/registry.d.ts +11 -9
  17. package/dist/accounts/registry.d.ts.map +1 -1
  18. package/dist/accounts/registry.js +169 -98
  19. package/dist/accounts/registry.js.map +1 -1
  20. package/dist/auth.d.ts +106 -0
  21. package/dist/auth.d.ts.map +1 -0
  22. package/dist/auth.js +455 -0
  23. package/dist/auth.js.map +1 -0
  24. package/dist/authorization.d.ts +9 -0
  25. package/dist/authorization.d.ts.map +1 -0
  26. package/dist/authorization.js +23 -0
  27. package/dist/authorization.js.map +1 -0
  28. package/dist/cli.d.ts +15 -1
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +1096 -403
  31. package/dist/cli.js.map +1 -1
  32. package/dist/cliInstances.d.ts +51 -0
  33. package/dist/cliInstances.d.ts.map +1 -0
  34. package/dist/cliInstances.js +237 -0
  35. package/dist/cliInstances.js.map +1 -0
  36. package/dist/cliMail.d.ts +9 -0
  37. package/dist/cliMail.d.ts.map +1 -0
  38. package/dist/cliMail.js +450 -0
  39. package/dist/cliMail.js.map +1 -0
  40. package/dist/config.d.ts +95 -118
  41. package/dist/config.d.ts.map +1 -1
  42. package/dist/config.js +351 -286
  43. package/dist/config.js.map +1 -1
  44. package/dist/configMigration.d.ts +8 -0
  45. package/dist/configMigration.d.ts.map +1 -0
  46. package/dist/configMigration.js +234 -0
  47. package/dist/configMigration.js.map +1 -0
  48. package/dist/context.d.ts +8 -2
  49. package/dist/context.d.ts.map +1 -1
  50. package/dist/context.js +29 -8
  51. package/dist/context.js.map +1 -1
  52. package/dist/deploymentConfig.d.ts +190 -0
  53. package/dist/deploymentConfig.d.ts.map +1 -0
  54. package/dist/deploymentConfig.js +539 -0
  55. package/dist/deploymentConfig.js.map +1 -0
  56. package/dist/http/admin.d.ts +75 -3
  57. package/dist/http/admin.d.ts.map +1 -1
  58. package/dist/http/admin.js +347 -79
  59. package/dist/http/admin.js.map +1 -1
  60. package/dist/http/app.d.ts +4 -1
  61. package/dist/http/app.d.ts.map +1 -1
  62. package/dist/http/app.js +119 -63
  63. package/dist/http/app.js.map +1 -1
  64. package/dist/http/identity.d.ts +114 -0
  65. package/dist/http/identity.d.ts.map +1 -0
  66. package/dist/http/identity.js +936 -0
  67. package/dist/http/identity.js.map +1 -0
  68. package/dist/http/operationRoutes.d.ts +8 -0
  69. package/dist/http/operationRoutes.d.ts.map +1 -0
  70. package/dist/http/operationRoutes.js +13 -0
  71. package/dist/http/operationRoutes.js.map +1 -0
  72. package/dist/http/rest.d.ts +16 -4
  73. package/dist/http/rest.d.ts.map +1 -1
  74. package/dist/http/rest.js +213 -71
  75. package/dist/http/rest.js.map +1 -1
  76. package/dist/index.d.ts +5 -3
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +5 -3
  79. package/dist/index.js.map +1 -1
  80. package/dist/instanceConfig.d.ts +36 -0
  81. package/dist/instanceConfig.d.ts.map +1 -0
  82. package/dist/instanceConfig.js +157 -0
  83. package/dist/instanceConfig.js.map +1 -0
  84. package/dist/licensing/activation.d.ts +2 -1
  85. package/dist/licensing/activation.d.ts.map +1 -1
  86. package/dist/licensing/activation.js +2 -3
  87. package/dist/licensing/activation.js.map +1 -1
  88. package/dist/licensing/entitlements.d.ts.map +1 -1
  89. package/dist/licensing/entitlements.js +2 -2
  90. package/dist/licensing/entitlements.js.map +1 -1
  91. package/dist/licensing/refresher.d.ts +6 -1
  92. package/dist/licensing/refresher.d.ts.map +1 -1
  93. package/dist/licensing/refresher.js +18 -7
  94. package/dist/licensing/refresher.js.map +1 -1
  95. package/dist/logging.d.ts +87 -0
  96. package/dist/logging.d.ts.map +1 -0
  97. package/dist/logging.js +612 -0
  98. package/dist/logging.js.map +1 -0
  99. package/dist/mcp/buildServer.d.ts +3 -1
  100. package/dist/mcp/buildServer.d.ts.map +1 -1
  101. package/dist/mcp/buildServer.js +27 -24
  102. package/dist/mcp/buildServer.js.map +1 -1
  103. package/dist/permissions.d.ts +1 -1
  104. package/dist/permissions.d.ts.map +1 -1
  105. package/dist/permissions.js +10 -4
  106. package/dist/permissions.js.map +1 -1
  107. package/dist/scheduler/sendScheduler.d.ts +4 -3
  108. package/dist/scheduler/sendScheduler.d.ts.map +1 -1
  109. package/dist/scheduler/sendScheduler.js +15 -3
  110. package/dist/scheduler/sendScheduler.js.map +1 -1
  111. package/dist/service/emailService.d.ts +10 -17
  112. package/dist/service/emailService.d.ts.map +1 -1
  113. package/dist/service/emailService.js +28 -38
  114. package/dist/service/emailService.js.map +1 -1
  115. package/dist/storage/adminAudit.d.ts +15 -2
  116. package/dist/storage/adminAudit.d.ts.map +1 -1
  117. package/dist/storage/adminAudit.js +22 -27
  118. package/dist/storage/adminAudit.js.map +1 -1
  119. package/dist/storage/apiKeys.d.ts +8 -8
  120. package/dist/storage/apiKeys.d.ts.map +1 -1
  121. package/dist/storage/apiKeys.js +16 -11
  122. package/dist/storage/apiKeys.js.map +1 -1
  123. package/dist/storage/db.d.ts +596 -31
  124. package/dist/storage/db.d.ts.map +1 -1
  125. package/dist/storage/db.js +383 -45
  126. package/dist/storage/db.js.map +1 -1
  127. package/dist/storage/fileLock.d.ts +7 -0
  128. package/dist/storage/fileLock.d.ts.map +1 -0
  129. package/dist/storage/fileLock.js +183 -0
  130. package/dist/storage/fileLock.js.map +1 -0
  131. package/dist/storage/gmailConnectionGrants.d.ts +3 -4
  132. package/dist/storage/gmailConnectionGrants.d.ts.map +1 -1
  133. package/dist/storage/gmailConnectionGrants.js +6 -6
  134. package/dist/storage/gmailConnectionGrants.js.map +1 -1
  135. package/dist/storage/members.d.ts +11 -0
  136. package/dist/storage/members.d.ts.map +1 -1
  137. package/dist/storage/members.js +70 -6
  138. package/dist/storage/members.js.map +1 -1
  139. package/dist/telemetry.d.ts +17 -2
  140. package/dist/telemetry.d.ts.map +1 -1
  141. package/dist/telemetry.js +34 -19
  142. package/dist/telemetry.js.map +1 -1
  143. package/dist/updateNotifier.d.ts +18 -0
  144. package/dist/updateNotifier.d.ts.map +1 -0
  145. package/dist/updateNotifier.js +128 -0
  146. package/dist/updateNotifier.js.map +1 -0
  147. package/package.json +38 -5
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/storage/db.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/storage/db.ts"],"names":[],"mappings":"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"}
@@ -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
- /** Owning member. NULL is retained only for migrated installs with no members. */
19
- memberId: text('member_id').references(() => members.id, { onDelete: 'set null' }),
20
- sharingMode: text('sharing_mode').notNull().default('private'),
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 accountMemberShares = sqliteTable('account_member_shares', {
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; NULL is reserved for migrated system credentials. */
76
- memberId: text('member_id').references(() => members.id, { onDelete: 'set null' }),
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
- memberId: text('member_id'),
177
+ ownerMemberId: text('owner_member_id'),
102
178
  reauthorizeAccountId: text('reauthorize_account_id'),
103
- sharingMode: text('sharing_mode'),
104
- sharedMemberIds: text('shared_member_ids'),
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 SET NULL,
166
- sharing_mode TEXT NOT NULL DEFAULT 'private'
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 account_member_shares (
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 SET NULL,
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
- member_id TEXT,
363
+ owner_member_id TEXT,
235
364
  reauthorize_account_id TEXT,
236
- sharing_mode TEXT,
237
- shared_member_ids TEXT,
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
- export function openDb(dbPath) {
285
- const sqlite = new Database(dbPath);
286
- sqlite.pragma('journal_mode = WAL');
287
- sqlite.pragma('foreign_keys = ON');
288
- sqlite.exec(BOOTSTRAP_SQL);
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('sharing_mode')) {
318
- sqlite.exec('ALTER TABLE accounts ADD COLUMN sharing_mode TEXT');
319
- sqlite.exec("UPDATE accounts SET sharing_mode = CASE WHEN member_id IS NULL THEN 'all' ELSE 'private' END");
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 account_member_shares (
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
- // Migrated ownerless mailboxes remain shared with all, but receive an owner
329
- // when the installation already has a member.
330
- sqlite.exec(`
331
- UPDATE accounts SET member_id = (
332
- SELECT id FROM members ORDER BY CASE role WHEN 'admin' THEN 0 ELSE 1 END, created_at, id LIMIT 1
333
- )
334
- WHERE member_id IS NULL AND sharing_mode = 'all' AND EXISTS (SELECT 1 FROM members)
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 that admin and memberless keys
349
- // held before administrative capabilities existed. Broader capabilities
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('sharing_mode')) {
378
- sqlite.exec('ALTER TABLE gmail_connection_grants ADD COLUMN sharing_mode TEXT');
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('shared_member_ids')) {
381
- sqlite.exec('ALTER TABLE gmail_connection_grants ADD COLUMN shared_member_ids TEXT');
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
- return drizzle(sqlite);
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