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/http/admin.js
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
242
|
+
ownerMemberId: identifier.optional(),
|
|
124
243
|
reauthorizeAccountId: identifier.optional(),
|
|
125
|
-
|
|
126
|
-
|
|
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',
|
|
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()
|
|
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
|
|
251
|
-
throw new AdminFailure('permission_denied', `This
|
|
398
|
+
if (!canAdminister(auth, capability)) {
|
|
399
|
+
throw new AdminFailure('permission_denied', `This credential does not allow ${capability}.`, 403);
|
|
252
400
|
}
|
|
253
|
-
|
|
254
|
-
|
|
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
|
|
264
|
-
if (input.reauthorizeAccountId &&
|
|
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.
|
|
270
|
-
throw new EmailError('invalid_request', '
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ?
|
|
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
|
-
|
|
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
|