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,14 +1,122 @@
1
1
  import { createRoute, z } from '@hono/zod-openapi';
2
+ import { isIP } from 'node:net';
2
3
  import { EmailError, isEmailError } from '@fluxmail/core';
3
4
  import { prepareHostedGmailConnection, prepareHostedOutlookConnection } from '../accounts/gmailConnection.js';
4
- import { LICENSE_KEY_PATTERN } from '../licensing/client.js';
5
+ import { EnvironmentControlledSettingError } from '../config.js';
6
+ import { LICENSE_KEY_PATTERN, releaseLicense } from '../licensing/client.js';
5
7
  import { activateLicense } from '../licensing/activation.js';
6
- import { checkLicenseState, readLeaseRow } from '../licensing/entitlements.js';
8
+ import { checkLicenseState, clearLease, readLeaseRow } from '../licensing/entitlements.js';
9
+ import { loadInstanceId } from '../licensing/refresher.js';
7
10
  import { ADMIN_CAPABILITIES, MCP_CAPABILITIES, NAMED_PERMISSION_PROFILES, customPermissionPolicy, permissionPolicyForProfile, } from '../permissions.js';
8
- import { countUsableRootKeys, createApiKey, isUsableRootKey, listApiKeys, revokeApiKey, updateApiKey, } from '../storage/apiKeys.js';
11
+ import { createApiKey, listApiKeys, revokeApiKey, updateApiKey } from '../storage/apiKeys.js';
9
12
  import { findMember } from '../storage/members.js';
13
+ import { canAdminister, canSeeAccountMetadata } from '../authorization.js';
14
+ import { operationIdForRequest } from './operationRoutes.js';
15
+ import { logCodedFailure, logFailure } from '../logging.js';
10
16
  export const ADMIN_BODY_LIMIT = 64 * 1024;
11
17
  export const ADMIN_CONNECTION_TIMEOUT_MS = 30_000;
18
+ export const adminOperationRoutes = {
19
+ createConnection: {
20
+ method: 'post',
21
+ path: '/api/v1/admin/connections',
22
+ operationId: 'createAdministrativeConnection',
23
+ },
24
+ testImapConnection: {
25
+ method: 'post',
26
+ path: '/api/v1/admin/imap/tests',
27
+ operationId: 'testAdministrativeImapConnection',
28
+ },
29
+ updateImapFolders: {
30
+ method: 'patch',
31
+ path: '/api/v1/admin/accounts/{accountId}/imap/folders',
32
+ operationId: 'updateAdministrativeImapFolders',
33
+ },
34
+ listApiKeys: {
35
+ method: 'get',
36
+ path: '/api/v1/admin/api-keys',
37
+ operationId: 'listAdministrativeApiKeys',
38
+ },
39
+ createApiKey: {
40
+ method: 'post',
41
+ path: '/api/v1/admin/api-keys',
42
+ operationId: 'createAdministrativeApiKey',
43
+ },
44
+ updateApiKey: {
45
+ method: 'patch',
46
+ path: '/api/v1/admin/api-keys/{keyId}',
47
+ operationId: 'updateAdministrativeApiKey',
48
+ },
49
+ revokeApiKey: {
50
+ method: 'delete',
51
+ path: '/api/v1/admin/api-keys/{keyId}',
52
+ operationId: 'revokeAdministrativeApiKey',
53
+ },
54
+ getOauthApps: {
55
+ method: 'get',
56
+ path: '/api/v1/admin/oauth-apps',
57
+ operationId: 'getAdministrativeOAuthApps',
58
+ },
59
+ putOauthApp: {
60
+ method: 'put',
61
+ path: '/api/v1/admin/oauth-apps/{provider}',
62
+ operationId: 'putAdministrativeOAuthApp',
63
+ },
64
+ deleteOauthApp: {
65
+ method: 'delete',
66
+ path: '/api/v1/admin/oauth-apps/{provider}',
67
+ operationId: 'deleteAdministrativeOAuthApp',
68
+ },
69
+ getLicense: {
70
+ method: 'get',
71
+ path: '/api/v1/admin/license',
72
+ operationId: 'getAdministrativeLicense',
73
+ },
74
+ activateLicense: {
75
+ method: 'post',
76
+ path: '/api/v1/admin/license/activate',
77
+ operationId: 'activateAdministrativeLicense',
78
+ },
79
+ deactivateLicense: {
80
+ method: 'delete',
81
+ path: '/api/v1/admin/license',
82
+ operationId: 'deactivateAdministrativeLicense',
83
+ },
84
+ };
85
+ export function adminOperationId(method, path) {
86
+ return operationIdForRequest(Object.values(adminOperationRoutes), method, path);
87
+ }
88
+ function firstForwardedElement(value) {
89
+ return value?.split(',', 1)[0]?.trim() || undefined;
90
+ }
91
+ function forwardedParameter(request, name) {
92
+ const first = firstForwardedElement(request.headers.get('forwarded'));
93
+ if (!first)
94
+ return undefined;
95
+ for (const parameter of first.split(';')) {
96
+ const [key, ...parts] = parameter.split('=');
97
+ if (key?.trim().toLowerCase() !== name)
98
+ continue;
99
+ return parts.join('=').trim().replace(/^"|"$/g, '');
100
+ }
101
+ return undefined;
102
+ }
103
+ function forwardedProtocol(request) {
104
+ return (forwardedParameter(request, 'proto') ?? firstForwardedElement(request.headers.get('x-forwarded-proto')))?.toLowerCase();
105
+ }
106
+ function forwardedAddress(request) {
107
+ const value = forwardedParameter(request, 'for') ?? firstForwardedElement(request.headers.get('x-forwarded-for'));
108
+ if (!value || value.toLowerCase() === 'unknown' || value.startsWith('_'))
109
+ return undefined;
110
+ if (value.startsWith('[')) {
111
+ const closingBracket = value.indexOf(']');
112
+ const address = closingBracket > 0 ? value.slice(1, closingBracket) : '';
113
+ return isIP(address) ? address : undefined;
114
+ }
115
+ if (isIP(value))
116
+ return value;
117
+ const ipv4WithPort = value.match(/^(.*):(\d+)$/)?.[1];
118
+ return ipv4WithPort && isIP(ipv4WithPort) === 4 ? ipv4WithPort : undefined;
119
+ }
12
120
  function isLoopbackAddress(value) {
13
121
  const normalized = value
14
122
  .toLowerCase()
@@ -19,17 +127,29 @@ function isLoopbackAddress(value) {
19
127
  function isLoopbackHostname(value) {
20
128
  return value.toLowerCase() === 'localhost' || isLoopbackAddress(value);
21
129
  }
22
- export function administrationUsesHttps(request, connection) {
130
+ export function administrationUsesHttps(request, connection, trustProxy = false) {
23
131
  if (connection?.encrypted)
24
132
  return true;
133
+ if (connection?.remoteAddress !== undefined && isLoopbackAddress(connection.remoteAddress))
134
+ return true;
135
+ if (trustProxy && forwardedProtocol(request) === 'https')
136
+ return true;
25
137
  if (connection?.remoteAddress !== undefined)
26
- return isLoopbackAddress(connection.remoteAddress);
138
+ return false;
27
139
  const requestUrl = new URL(request.url);
28
140
  // No socket is available in Hono's in-memory helper or in non-Node adapters.
29
141
  // These paths use the request URL itself. Node requests must pass the peer
30
142
  // and TLS state above so headers and configuration cannot bless plaintext.
31
143
  return requestUrl.protocol === 'https:' || isLoopbackHostname(requestUrl.hostname);
32
144
  }
145
+ export function requestClientAddress(request, connection, trustProxy = false) {
146
+ if (trustProxy) {
147
+ const forwarded = forwardedAddress(request);
148
+ if (forwarded)
149
+ return forwarded;
150
+ }
151
+ return connection?.remoteAddress ?? 'unknown';
152
+ }
33
153
  export async function requestBodyExceedsLimit(request, limit = ADMIN_BODY_LIMIT) {
34
154
  const contentLength = request.headers.get('content-length');
35
155
  if (contentLength !== null) {
@@ -65,7 +185,6 @@ const username = z.string().min(1).max(320);
65
185
  const password = z.string().min(1).max(4096);
66
186
  const folderPath = z.string().min(1).max(1024);
67
187
  const security = z.enum(['tls', 'starttls']);
68
- const sharingMode = z.enum(['private', 'selected', 'all']);
69
188
  const folderRoles = ['sent', 'drafts', 'trash', 'archive', 'spam'];
70
189
  const FolderOverridesSchema = z
71
190
  .object({
@@ -120,10 +239,10 @@ const ImapSettingsSchema = z
120
239
  },
121
240
  });
122
241
  const SharingSchema = {
123
- owner: identifier.optional(),
242
+ ownerMemberId: identifier.optional(),
124
243
  reauthorizeAccountId: identifier.optional(),
125
- sharingMode: sharingMode.optional(),
126
- shareWith: z.array(identifier).max(100).optional(),
244
+ sharedWithAll: z.boolean().optional(),
245
+ grantedMemberIds: z.array(identifier).max(100).optional(),
127
246
  };
128
247
  const ConnectionSchema = z
129
248
  .discriminatedUnion('provider', [
@@ -131,7 +250,7 @@ const ConnectionSchema = z
131
250
  z.object({ provider: z.literal('outlook'), ...SharingSchema }).strict(),
132
251
  z.object({ provider: z.literal('imap'), ...SharingSchema, ...ImapSettingsSchema.shape }).strict(),
133
252
  ])
134
- .openapi({ example: { provider: 'gmail', owner: 'you@example.com' } });
253
+ .openapi({ example: { provider: 'gmail', ownerMemberId: 'you@example.com' } });
135
254
  const CapabilitySchema = z.enum([...MCP_CAPABILITIES, ...ADMIN_CAPABILITIES]);
136
255
  const AdminCapabilitySchema = z.enum(ADMIN_CAPABILITIES);
137
256
  const ApiKeyCreateSchema = z
@@ -195,7 +314,7 @@ const ApiKeySchema = z
195
314
  name: z.string(),
196
315
  createdAt: z.string(),
197
316
  lastUsedAt: z.string().nullable(),
198
- memberId: z.string().nullable(),
317
+ memberId: z.string(),
199
318
  permissionProfile: z.enum([...NAMED_PERMISSION_PROFILES, 'custom']),
200
319
  capabilities: z.array(CapabilitySchema),
201
320
  supplementalCapabilities: z.array(AdminCapabilitySchema),
@@ -205,6 +324,35 @@ const ApiKeySchema = z
205
324
  .openapi('AdministrativeApiKey');
206
325
  const GenericDataSchema = z.object({ data: z.record(z.unknown()) }).strict();
207
326
  const ApiKeyCreateResponseSchema = z.object({ data: ApiKeySchema.extend({ key: z.string() }) }).strict();
327
+ const OAuthProviderSchema = z.enum(['google', 'outlook']).openapi({ example: 'google' });
328
+ const OAuthAppInputSchema = z
329
+ .object({
330
+ clientId: z.string().trim().min(1).max(2048).openapi({ example: 'client-id.apps.example.com' }),
331
+ clientSecret: z.string().trim().min(1).max(16_384).optional().openapi({ example: 'client-secret' }),
332
+ tenantId: z.string().trim().min(1).max(2048).optional().openapi({ example: 'common' }),
333
+ publicClient: z.boolean().optional(),
334
+ })
335
+ .strict()
336
+ .openapi({ example: { clientId: 'client-id.apps.example.com', clientSecret: 'client-secret' } });
337
+ const OAuthAppsResponseSchema = z
338
+ .object({
339
+ data: z.object({
340
+ google: z.object({
341
+ clientId: z.string(),
342
+ clientSecretConfigured: z.boolean(),
343
+ source: z.enum(['built-in', 'stored', 'environment', 'environment-file']),
344
+ mutable: z.boolean(),
345
+ }),
346
+ outlook: z.object({
347
+ clientId: z.string().nullable(),
348
+ tenantId: z.string().nullable(),
349
+ clientSecretConfigured: z.boolean(),
350
+ source: z.enum(['stored', 'environment', 'environment-file']).nullable(),
351
+ mutable: z.boolean(),
352
+ }),
353
+ }),
354
+ })
355
+ .strict();
208
356
  const errors = {
209
357
  400: { content: { 'application/json': { schema: ErrorSchema } }, description: 'Invalid request' },
210
358
  401: { content: { 'application/json': { schema: ErrorSchema } }, description: 'Authentication required' },
@@ -247,11 +395,18 @@ function json(body, status = 200) {
247
395
  });
248
396
  }
249
397
  function requireAdmin(auth, capability) {
250
- if (!auth.permissions.capabilities.includes(capability)) {
251
- throw new AdminFailure('permission_denied', `This API key does not allow ${capability}.`, 403);
398
+ if (!canAdminister(auth, capability)) {
399
+ throw new AdminFailure('permission_denied', `This credential does not allow ${capability}.`, 403);
252
400
  }
253
- if (auth.memberId !== null && auth.role !== 'admin') {
254
- throw new AdminFailure('admin_role_required', 'The API key owner is not an admin member.', 403);
401
+ }
402
+ function requireAdminAccount(auth, account) {
403
+ if (!canSeeAccountMetadata(auth, account)) {
404
+ throw new EmailError('not_found', 'Mailbox not found.');
405
+ }
406
+ }
407
+ function requireUnscopedAccountCreation(auth) {
408
+ if (auth.accountIds !== null) {
409
+ throw new EmailError('permission_denied', 'This API key is limited to existing mailboxes.');
255
410
  }
256
411
  }
257
412
  function registry(deps) {
@@ -260,23 +415,20 @@ function registry(deps) {
260
415
  return deps.registry;
261
416
  }
262
417
  function resolveSharing(deps, input) {
263
- const shareWith = input.shareWith ?? [];
264
- if (input.reauthorizeAccountId && (input.owner || input.sharingMode || shareWith.length)) {
418
+ const grantedMemberIds = input.grantedMemberIds ?? [];
419
+ if (input.reauthorizeAccountId &&
420
+ (input.ownerMemberId || input.sharedWithAll !== undefined || grantedMemberIds.length)) {
265
421
  throw new EmailError('invalid_request', 'Ownership and sharing settings cannot be combined with reauthorizeAccountId.');
266
422
  }
267
423
  if (input.reauthorizeAccountId)
268
424
  return { reauthorizeAccountId: registry(deps).getAccount(input.reauthorizeAccountId).id };
269
- if (!input.owner)
270
- throw new EmailError('invalid_request', 'owner is required for a new mailbox.');
271
- const memberId = findMember(deps.db, input.owner).id;
272
- const mode = input.sharingMode ?? (shareWith.length ? 'selected' : 'private');
273
- if (mode === 'selected' && !shareWith.length) {
274
- throw new EmailError('invalid_request', 'shareWith is required when sharingMode is "selected".');
275
- }
276
- if (mode !== 'selected' && shareWith.length) {
277
- throw new EmailError('invalid_request', 'shareWith can only be used when sharingMode is "selected".');
278
- }
279
- return { memberId, sharingMode: mode, sharedMemberIds: shareWith.map((ref) => findMember(deps.db, ref).id) };
425
+ if (!input.ownerMemberId)
426
+ throw new EmailError('invalid_request', 'ownerMemberId is required for a new mailbox.');
427
+ return {
428
+ ownerMemberId: findMember(deps.db, input.ownerMemberId).id,
429
+ sharedWithAll: input.sharedWithAll ?? false,
430
+ grantedMemberIds: grantedMemberIds.map((ref) => findMember(deps.db, ref).id),
431
+ };
280
432
  }
281
433
  function permissions(input) {
282
434
  return input.customCapabilities
@@ -295,19 +447,27 @@ function serializeApiKey(key) {
295
447
  lastUsedAt: key.lastUsedAt === null ? null : new Date(key.lastUsedAt).toISOString(),
296
448
  };
297
449
  }
298
- async function run(fn) {
450
+ async function run(deps, fn) {
299
451
  try {
300
452
  return await fn();
301
453
  }
302
454
  catch (error) {
455
+ const context = {
456
+ productSurface: 'rest',
457
+ operation: 'administrative_request',
458
+ };
459
+ if (error instanceof AdminFailure) {
460
+ logCodedFailure(deps.logger, 'rest.operation_failed', error.code, error.message, context);
461
+ }
462
+ else {
463
+ logFailure(deps.logger, 'rest.operation_failed', error, context);
464
+ }
303
465
  return failureResponse(error);
304
466
  }
305
467
  }
306
468
  export function registerAdminRoutes(app, deps) {
307
469
  const connectionsRoute = createRoute({
308
- method: 'post',
309
- path: '/api/v1/admin/connections',
310
- operationId: 'createAdministrativeConnection',
470
+ ...adminOperationRoutes.createConnection,
311
471
  summary: 'Create or reauthorize a connection',
312
472
  description: 'Create or reauthorize a Gmail, Outlook, or IMAP connection. Requires admin.accounts.',
313
473
  ...protectedRoute,
@@ -320,10 +480,17 @@ export function registerAdminRoutes(app, deps) {
320
480
  ...errors,
321
481
  },
322
482
  });
323
- app.openapi(connectionsRoute, (c) => run(async () => {
324
- requireAdmin(c.get('restAuth'), 'admin.accounts');
483
+ app.openapi(connectionsRoute, (c) => run(deps, async () => {
484
+ const auth = c.get('restAuth');
485
+ requireAdmin(auth, 'admin.accounts');
325
486
  const input = c.req.valid('json');
326
487
  const access = resolveSharing(deps, input);
488
+ if (access.reauthorizeAccountId) {
489
+ requireAdminAccount(auth, registry(deps).getAccount(access.reauthorizeAccountId));
490
+ }
491
+ else {
492
+ requireUnscopedAccountCreation(auth);
493
+ }
327
494
  if (input.provider === 'gmail' || input.provider === 'outlook') {
328
495
  if (!access.reauthorizeAccountId)
329
496
  registry(deps).assertCanAddAccount();
@@ -358,13 +525,13 @@ export function registerAdminRoutes(app, deps) {
358
525
  folderOverrides: { ...previous?.folderOverrides, ...input.folderOverrides },
359
526
  };
360
527
  const warnings = await registry(deps).testImapCredentials(input.email, credentials, input.displayName, ADMIN_CONNECTION_TIMEOUT_MS);
361
- const connected = registry(deps).addImapAccount(input.email, credentials, input.displayName, access.memberId, access.reauthorizeAccountId, access.sharingMode ? { sharingMode: access.sharingMode, sharedMemberIds: access.sharedMemberIds } : undefined);
528
+ const connected = registry(deps).addImapAccount(input.email, credentials, input.displayName, access.ownerMemberId, access.reauthorizeAccountId, access.ownerMemberId
529
+ ? { sharedWithAll: access.sharedWithAll ?? false, grantedMemberIds: access.grantedMemberIds }
530
+ : undefined);
362
531
  return json({ data: { account: connected, warnings } }, 201);
363
532
  }));
364
533
  const imapTestRoute = createRoute({
365
- method: 'post',
366
- path: '/api/v1/admin/imap/tests',
367
- operationId: 'testAdministrativeImapConnection',
534
+ ...adminOperationRoutes.testImapConnection,
368
535
  summary: 'Test an IMAP connection',
369
536
  description: 'Test IMAP and SMTP settings without saving an account. Requires admin.accounts.',
370
537
  ...protectedRoute,
@@ -374,7 +541,7 @@ export function registerAdminRoutes(app, deps) {
374
541
  ...errors,
375
542
  },
376
543
  });
377
- app.openapi(imapTestRoute, (c) => run(async () => {
544
+ app.openapi(imapTestRoute, (c) => run(deps, async () => {
378
545
  requireAdmin(c.get('restAuth'), 'admin.accounts');
379
546
  const input = c.req.valid('json');
380
547
  const warnings = await registry(deps).testImapCredentials(input.email, {
@@ -386,9 +553,7 @@ export function registerAdminRoutes(app, deps) {
386
553
  return json({ data: { ok: true, warnings } });
387
554
  }));
388
555
  const folderRoute = createRoute({
389
- method: 'patch',
390
- path: '/api/v1/admin/accounts/{accountId}/imap/folders',
391
- operationId: 'updateAdministrativeImapFolders',
556
+ ...adminOperationRoutes.updateImapFolders,
392
557
  summary: 'Update IMAP folders',
393
558
  description: 'Update the folder overrides for an IMAP account. Requires admin.accounts.',
394
559
  ...protectedRoute,
@@ -401,11 +566,13 @@ export function registerAdminRoutes(app, deps) {
401
566
  ...errors,
402
567
  },
403
568
  });
404
- app.openapi(folderRoute, (c) => run(async () => {
405
- requireAdmin(c.get('restAuth'), 'admin.accounts');
569
+ app.openapi(folderRoute, (c) => run(deps, async () => {
570
+ const auth = c.get('restAuth');
571
+ requireAdmin(auth, 'admin.accounts');
406
572
  const { accountId } = c.req.valid('param');
407
573
  const patch = c.req.valid('json');
408
574
  const account = registry(deps).getAccount(accountId);
575
+ requireAdminAccount(auth, account);
409
576
  const current = registry(deps).loadImapCredentials(account.id);
410
577
  const nextOverrides = { ...current.folderOverrides };
411
578
  for (const role of folderRoles) {
@@ -426,9 +593,7 @@ export function registerAdminRoutes(app, deps) {
426
593
  return json({ data: { accountId: account.id, folderOverrides: nextOverrides, warnings } });
427
594
  }));
428
595
  const listKeysRoute = createRoute({
429
- method: 'get',
430
- path: '/api/v1/admin/api-keys',
431
- operationId: 'listAdministrativeApiKeys',
596
+ ...adminOperationRoutes.listApiKeys,
432
597
  summary: 'List API keys',
433
598
  description: 'List API key metadata without returning plaintext secrets. Requires admin.api_keys.',
434
599
  ...protectedRoute,
@@ -440,14 +605,12 @@ export function registerAdminRoutes(app, deps) {
440
605
  ...errors,
441
606
  },
442
607
  });
443
- app.openapi(listKeysRoute, (c) => run(() => {
608
+ app.openapi(listKeysRoute, (c) => run(deps, () => {
444
609
  requireAdmin(c.get('restAuth'), 'admin.api_keys');
445
610
  return json({ data: listApiKeys(deps.db).map(serializeApiKey) });
446
611
  }));
447
612
  const createKeyRoute = createRoute({
448
- method: 'post',
449
- path: '/api/v1/admin/api-keys',
450
- operationId: 'createAdministrativeApiKey',
613
+ ...adminOperationRoutes.createApiKey,
451
614
  summary: 'Create an API key',
452
615
  description: 'Create an API key and return its plaintext secret once. Requires admin.api_keys.',
453
616
  ...protectedRoute,
@@ -460,7 +623,7 @@ export function registerAdminRoutes(app, deps) {
460
623
  ...errors,
461
624
  },
462
625
  });
463
- app.openapi(createKeyRoute, (c) => run(() => {
626
+ app.openapi(createKeyRoute, (c) => run(deps, () => {
464
627
  requireAdmin(c.get('restAuth'), 'admin.api_keys');
465
628
  const input = c.req.valid('json');
466
629
  const member = findMember(deps.db, input.member);
@@ -468,9 +631,7 @@ export function registerAdminRoutes(app, deps) {
468
631
  return json({ data: { ...serializeApiKey(created.info), key: created.key } }, 201);
469
632
  }));
470
633
  const patchKeyRoute = createRoute({
471
- method: 'patch',
472
- path: '/api/v1/admin/api-keys/{keyId}',
473
- operationId: 'updateAdministrativeApiKey',
634
+ ...adminOperationRoutes.updateApiKey,
474
635
  summary: 'Update an API key',
475
636
  description: 'Update the permissions or mailbox scope of an API key. Requires admin.api_keys.',
476
637
  ...protectedRoute,
@@ -486,7 +647,7 @@ export function registerAdminRoutes(app, deps) {
486
647
  ...errors,
487
648
  },
488
649
  });
489
- app.openapi(patchKeyRoute, (c) => run(() => {
650
+ app.openapi(patchKeyRoute, (c) => run(deps, () => {
490
651
  requireAdmin(c.get('restAuth'), 'admin.api_keys');
491
652
  const { keyId } = c.req.valid('param');
492
653
  const input = c.req.valid('json');
@@ -503,12 +664,6 @@ export function registerAdminRoutes(app, deps) {
503
664
  }
504
665
  nextPermissions = permissionPolicyForProfile(existing.permissionProfile, input.supplementalCapabilities);
505
666
  }
506
- if (isUsableRootKey(deps.db, keyId) &&
507
- nextPermissions &&
508
- !nextPermissions.capabilities.includes('admin.api_keys') &&
509
- countUsableRootKeys(deps.db, keyId) === 0) {
510
- throw new AdminFailure('last_root_key', 'Another usable admin.api_keys credential is required first.', 409);
511
- }
512
667
  const updated = updateApiKey(deps.db, keyId, {
513
668
  ...(nextPermissions ? { permissions: nextPermissions } : {}),
514
669
  ...(input.accounts !== undefined ? { accountIds: accountIds(deps, input.accounts) } : {}),
@@ -516,9 +671,7 @@ export function registerAdminRoutes(app, deps) {
516
671
  return json({ data: serializeApiKey(updated) });
517
672
  }));
518
673
  const deleteKeyRoute = createRoute({
519
- method: 'delete',
520
- path: '/api/v1/admin/api-keys/{keyId}',
521
- operationId: 'revokeAdministrativeApiKey',
674
+ ...adminOperationRoutes.revokeApiKey,
522
675
  summary: 'Revoke an API key',
523
676
  description: 'Revoke an API key. Requires admin.api_keys.',
524
677
  ...protectedRoute,
@@ -528,20 +681,107 @@ export function registerAdminRoutes(app, deps) {
528
681
  ...errors,
529
682
  },
530
683
  });
531
- app.openapi(deleteKeyRoute, (c) => run(() => {
684
+ app.openapi(deleteKeyRoute, (c) => run(deps, () => {
532
685
  requireAdmin(c.get('restAuth'), 'admin.api_keys');
533
686
  const { keyId } = c.req.valid('param');
534
- if (isUsableRootKey(deps.db, keyId) && countUsableRootKeys(deps.db, keyId) === 0) {
535
- throw new AdminFailure('last_root_key', 'Another usable admin.api_keys credential is required first.', 409);
536
- }
537
687
  if (!revokeApiKey(deps.db, keyId))
538
688
  throw new EmailError('not_found', `No API key with id "${keyId}".`);
539
689
  return json({ data: { id: keyId, revoked: true } });
540
690
  }));
691
+ const getOauthAppsRoute = createRoute({
692
+ ...adminOperationRoutes.getOauthApps,
693
+ summary: 'Get OAuth application status',
694
+ description: 'Get safe OAuth application metadata without returning client secrets. Requires admin.accounts.',
695
+ ...protectedRoute,
696
+ responses: {
697
+ 200: { content: { 'application/json': { schema: OAuthAppsResponseSchema } }, description: 'OAuth applications' },
698
+ ...errors,
699
+ },
700
+ });
701
+ app.openapi(getOauthAppsRoute, (c) => run(deps, () => {
702
+ requireAdmin(c.get('restAuth'), 'admin.accounts');
703
+ return json({ data: deps.configuration.oauthStatus() });
704
+ }));
705
+ const putOauthAppRoute = createRoute({
706
+ ...adminOperationRoutes.putOauthApp,
707
+ summary: 'Configure an OAuth application',
708
+ description: 'Save an OAuth application in encrypted instance settings. Requires admin.accounts.',
709
+ ...protectedRoute,
710
+ request: {
711
+ params: z.object({ provider: OAuthProviderSchema }).strict(),
712
+ body: { required: true, content: { 'application/json': { schema: OAuthAppInputSchema } } },
713
+ },
714
+ responses: {
715
+ 200: { content: { 'application/json': { schema: OAuthAppsResponseSchema } }, description: 'OAuth applications' },
716
+ ...errors,
717
+ },
718
+ });
719
+ app.openapi(putOauthAppRoute, (c) => run(deps, () => {
720
+ requireAdmin(c.get('restAuth'), 'admin.accounts');
721
+ const { provider } = c.req.valid('param');
722
+ const input = c.req.valid('json');
723
+ try {
724
+ if (provider === 'google') {
725
+ if (!input.clientSecret) {
726
+ throw new AdminFailure('invalid_request', 'Google OAuth requires a client secret.', 400);
727
+ }
728
+ if (input.tenantId !== undefined || input.publicClient !== undefined) {
729
+ throw new AdminFailure('invalid_request', 'Google OAuth does not accept Microsoft settings.', 400);
730
+ }
731
+ deps.configuration.setGoogle({ clientId: input.clientId, clientSecret: input.clientSecret });
732
+ }
733
+ else {
734
+ if (input.publicClient === true && input.clientSecret) {
735
+ throw new AdminFailure('invalid_request', 'A public client cannot include a client secret.', 400);
736
+ }
737
+ if (input.publicClient !== true && !input.clientSecret) {
738
+ throw new AdminFailure('invalid_request', 'Choose a public client or provide a client secret.', 400);
739
+ }
740
+ deps.configuration.setMicrosoft({
741
+ clientId: input.clientId,
742
+ tenantId: input.tenantId ?? 'common',
743
+ ...(input.clientSecret ? { clientSecret: input.clientSecret } : {}),
744
+ });
745
+ }
746
+ }
747
+ catch (error) {
748
+ if (error instanceof AdminFailure)
749
+ throw error;
750
+ if (!(error instanceof EnvironmentControlledSettingError))
751
+ throw error;
752
+ throw new AdminFailure('oauth_from_environment', 'Remove the OAuth environment overrides before changing this application.', 409);
753
+ }
754
+ return json({ data: deps.configuration.oauthStatus() });
755
+ }));
756
+ const deleteOauthAppRoute = createRoute({
757
+ ...adminOperationRoutes.deleteOauthApp,
758
+ summary: 'Reset an OAuth application',
759
+ description: 'Remove a stored OAuth application. Requires admin.accounts.',
760
+ ...protectedRoute,
761
+ request: { params: z.object({ provider: OAuthProviderSchema }).strict() },
762
+ responses: {
763
+ 200: { content: { 'application/json': { schema: OAuthAppsResponseSchema } }, description: 'OAuth applications' },
764
+ ...errors,
765
+ },
766
+ });
767
+ app.openapi(deleteOauthAppRoute, (c) => run(deps, () => {
768
+ requireAdmin(c.get('restAuth'), 'admin.accounts');
769
+ const { provider } = c.req.valid('param');
770
+ try {
771
+ if (provider === 'google')
772
+ deps.configuration.resetGoogle();
773
+ else
774
+ deps.configuration.resetMicrosoft();
775
+ }
776
+ catch (error) {
777
+ if (!(error instanceof EnvironmentControlledSettingError))
778
+ throw error;
779
+ throw new AdminFailure('oauth_from_environment', 'Remove the OAuth environment overrides before resetting this application.', 409);
780
+ }
781
+ return json({ data: deps.configuration.oauthStatus() });
782
+ }));
541
783
  const licenseRoute = createRoute({
542
- method: 'get',
543
- path: '/api/v1/admin/license',
544
- operationId: 'getAdministrativeLicense',
784
+ ...adminOperationRoutes.getLicense,
545
785
  summary: 'Get license status',
546
786
  description: 'Get license status and usage without returning the configured license key. Requires admin.license.',
547
787
  ...protectedRoute,
@@ -553,7 +793,7 @@ export function registerAdminRoutes(app, deps) {
553
793
  ...errors,
554
794
  },
555
795
  });
556
- app.openapi(licenseRoute, (c) => run(() => {
796
+ app.openapi(licenseRoute, (c) => run(deps, () => {
557
797
  requireAdmin(c.get('restAuth'), 'admin.license');
558
798
  const state = checkLicenseState(deps.db);
559
799
  const row = readLeaseRow(deps.db);
@@ -561,7 +801,7 @@ export function registerAdminRoutes(app, deps) {
561
801
  return json({
562
802
  data: {
563
803
  configured,
564
- source: configured ? (deps.config.licenseKeyFromEnvironment ? 'environment' : 'stored') : null,
804
+ source: configured ? deps.configuration.licenseSource() : null,
565
805
  entitlements: state.entitlements,
566
806
  usage: { accounts: state.accountCount, members: state.memberCount, overQuota: state.overQuota },
567
807
  lastValidatedAt: row ? new Date(row.updatedAt).toISOString() : null,
@@ -570,9 +810,7 @@ export function registerAdminRoutes(app, deps) {
570
810
  });
571
811
  }));
572
812
  const activateRoute = createRoute({
573
- method: 'post',
574
- path: '/api/v1/admin/license/activate',
575
- operationId: 'activateAdministrativeLicense',
813
+ ...adminOperationRoutes.activateLicense,
576
814
  summary: 'Activate a license',
577
815
  description: 'Validate and activate a Fluxmail license key. Requires admin.license.',
578
816
  ...protectedRoute,
@@ -586,7 +824,7 @@ export function registerAdminRoutes(app, deps) {
586
824
  ...errors,
587
825
  },
588
826
  });
589
- app.openapi(activateRoute, (c) => run(async () => {
827
+ app.openapi(activateRoute, (c) => run(deps, async () => {
590
828
  requireAdmin(c.get('restAuth'), 'admin.license');
591
829
  const { licenseKey } = c.req.valid('json');
592
830
  if (!LICENSE_KEY_PATTERN.test(licenseKey)) {
@@ -595,7 +833,7 @@ export function registerAdminRoutes(app, deps) {
595
833
  if (deps.config.licenseKeyFromEnvironment) {
596
834
  throw new AdminFailure('license_from_environment', 'REST cannot replace FLUXMAIL_LICENSE_KEY from the environment.', 409);
597
835
  }
598
- const result = await activateLicense(deps.db, {
836
+ const result = await activateLicense(deps.db, deps.configuration, {
599
837
  licenseKey,
600
838
  serverUrl: deps.config.licenseServerUrl,
601
839
  dataDir: deps.config.dataDir,
@@ -614,5 +852,35 @@ export function registerAdminRoutes(app, deps) {
614
852
  const status = result.outcome === 'in_use' ? 409 : result.outcome === 'inactive' ? 403 : 400;
615
853
  throw new AdminFailure(`license_${result.outcome}`, result.message, status);
616
854
  }));
855
+ const deactivateRoute = createRoute({
856
+ method: 'delete',
857
+ path: '/api/v1/admin/license',
858
+ operationId: 'deactivateAdministrativeLicense',
859
+ summary: 'Deactivate a license',
860
+ description: 'Release the stored license and return this instance to Personal limits. Requires admin.license.',
861
+ ...protectedRoute,
862
+ responses: {
863
+ 200: { content: { 'application/json': { schema: GenericDataSchema } }, description: 'License deactivated' },
864
+ ...errors,
865
+ },
866
+ });
867
+ app.openapi(deactivateRoute, (c) => run(deps, async () => {
868
+ requireAdmin(c.get('restAuth'), 'admin.license');
869
+ if (deps.config.licenseKeyFromEnvironment) {
870
+ throw new AdminFailure('license_from_environment', 'REST cannot remove FLUXMAIL_LICENSE_KEY from the environment.', 409);
871
+ }
872
+ const licenseKey = deps.licenseController?.configuredKey() ?? deps.config.licenseKey;
873
+ const released = licenseKey
874
+ ? await releaseLicense({
875
+ serverUrl: deps.config.licenseServerUrl,
876
+ licenseKey,
877
+ instanceId: loadInstanceId(deps.config.dataDir),
878
+ })
879
+ : false;
880
+ const removed = deps.configuration.removeLicenseKey();
881
+ clearLease(deps.db);
882
+ deps.licenseController?.wake();
883
+ return json({ data: { deactivated: true, released, removedStoredKey: removed } });
884
+ }));
617
885
  }
618
886
  //# sourceMappingURL=admin.js.map