@serve.zone/dcrouter 15.9.0 → 15.11.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/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/classes.dcrouter.js +2 -2
- package/dist_ts/email/classes.accepted-email-spool.d.ts +4 -1
- package/dist_ts/email/classes.accepted-email-spool.js +45 -2
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +14 -1
- package/dist_ts/email/classes.workapp-mail-manager.js +138 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.d.ts +5 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +111 -12
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/package.json +3 -3
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +1 -1
- package/ts/email/classes.accepted-email-spool.ts +68 -3
- package/ts/email/classes.workapp-mail-manager.ts +194 -1
- package/ts/opsserver/handlers/gatewayclient.handler.ts +135 -10
- package/ts/readme.md +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
} from '@push.rocks/smartmta';
|
|
8
8
|
import type { Buffer } from 'node:buffer';
|
|
9
9
|
import * as plugins from '../plugins.js';
|
|
10
|
-
import
|
|
10
|
+
import { CachedEmail } from '../db/index.js';
|
|
11
11
|
import type { DcRouter } from '../classes.dcrouter.js';
|
|
12
12
|
import type * as interfaces from '../../ts_interfaces/index.js';
|
|
13
13
|
|
|
@@ -30,8 +30,38 @@ type TMailAddressBindingSyncResponse = plugins.servezoneInterfaces.requests.mail
|
|
|
30
30
|
type TMailAddressBindingDeleteResponse = plugins.servezoneInterfaces.requests.mail.IReq_DeleteMailAddressBinding['response'];
|
|
31
31
|
type TMailCredentialRotateResponse = plugins.servezoneInterfaces.requests.mail.IReq_RotateMailCredential['response'];
|
|
32
32
|
type TMailEnqueueResponse = plugins.servezoneInterfaces.requests.mail.IReq_EnqueueMail['response'];
|
|
33
|
+
type TMailDeliveryStatusResponse = plugins.servezoneInterfaces.requests.mail.IReq_GetMailDeliveryStatus['response'];
|
|
33
34
|
type TMailEndpointRegistrationResponse = plugins.servezoneInterfaces.requests.mail.IReq_RegisterServiceMailEndpoint['response'];
|
|
34
35
|
type TWorkAppMailBinding = plugins.servezoneInterfaces.data.IWorkAppMailBinding;
|
|
36
|
+
type TCachedEmailWithTimestamps = CachedEmail & {
|
|
37
|
+
createdAt?: Date | number | string;
|
|
38
|
+
updatedAt?: Date | number | string;
|
|
39
|
+
};
|
|
40
|
+
type TSmartMtaStatusQueueItem = {
|
|
41
|
+
status?: 'pending' | 'processing' | 'queued' | 'delivered' | 'failed' | 'deferred';
|
|
42
|
+
attempts?: number;
|
|
43
|
+
nextAttempt?: Date;
|
|
44
|
+
lastError?: string;
|
|
45
|
+
processingResult?: {
|
|
46
|
+
headers?: Record<string, string>;
|
|
47
|
+
email?: {
|
|
48
|
+
headers?: Record<string, string>;
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
type TCachedEmailRouteData = {
|
|
53
|
+
session?: {
|
|
54
|
+
user?: {
|
|
55
|
+
username?: string;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
smartMta?: {
|
|
59
|
+
status?: 'queued' | 'deferred' | 'delivered' | 'failed';
|
|
60
|
+
nextAttempt?: string;
|
|
61
|
+
lastError?: string;
|
|
62
|
+
updatedAt?: string;
|
|
63
|
+
};
|
|
64
|
+
};
|
|
35
65
|
|
|
36
66
|
interface IStoredWorkAppMailIdentity {
|
|
37
67
|
id: string;
|
|
@@ -370,6 +400,31 @@ export class WorkAppMailManager {
|
|
|
370
400
|
}
|
|
371
401
|
}
|
|
372
402
|
|
|
403
|
+
public async getMailDeliveryStatus(
|
|
404
|
+
spoolItemIdArg: string,
|
|
405
|
+
): Promise<TMailDeliveryStatusResponse> {
|
|
406
|
+
const cachedEmail = await this.getCachedEmailById(spoolItemIdArg);
|
|
407
|
+
if (!cachedEmail) {
|
|
408
|
+
return { attempts: [] };
|
|
409
|
+
}
|
|
410
|
+
return await this.buildMailDeliveryStatus(cachedEmail);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
public async getMailDeliveryStatusForCredential(
|
|
414
|
+
authArg: plugins.servezoneInterfaces.requests.mail.IMailSubmissionRequestAuth,
|
|
415
|
+
spoolItemIdArg: string,
|
|
416
|
+
): Promise<TMailDeliveryStatusResponse> {
|
|
417
|
+
const identity = await this.authenticateMailCredential(authArg);
|
|
418
|
+
const cachedEmail = await this.getCachedEmailById(spoolItemIdArg);
|
|
419
|
+
if (!cachedEmail) {
|
|
420
|
+
return { attempts: [] };
|
|
421
|
+
}
|
|
422
|
+
if (!this.cachedEmailBelongsToIdentity(cachedEmail, identity)) {
|
|
423
|
+
throw new Error('Mail credential is not authorized for this spool item');
|
|
424
|
+
}
|
|
425
|
+
return await this.buildMailDeliveryStatus(cachedEmail);
|
|
426
|
+
}
|
|
427
|
+
|
|
373
428
|
public async registerServiceMailEndpoint(
|
|
374
429
|
authArg: plugins.servezoneInterfaces.requests.mail.IMailSubmissionRequestAuth,
|
|
375
430
|
peerArg: { tags?: Set<string> },
|
|
@@ -889,6 +944,144 @@ export class WorkAppMailManager {
|
|
|
889
944
|
return this.normalizeInboundTarget(identity.inboundTarget || this.toMailInboundTarget(identity.inbound));
|
|
890
945
|
}
|
|
891
946
|
|
|
947
|
+
private async getCachedEmailById(spoolItemIdArg: string): Promise<CachedEmail | null> {
|
|
948
|
+
const spoolItemId = String(spoolItemIdArg || '').trim();
|
|
949
|
+
if (!spoolItemId || !this.dcRouterRef.dcRouterDb?.isReady()) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
return await CachedEmail.findById(spoolItemId);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
private async buildMailDeliveryStatus(
|
|
956
|
+
cachedEmailArg: CachedEmail,
|
|
957
|
+
): Promise<TMailDeliveryStatusResponse> {
|
|
958
|
+
const queueItem = this.findQueueItemForCachedEmail(cachedEmailArg.id);
|
|
959
|
+
const routeData = this.parseCachedEmailRouteData(cachedEmailArg);
|
|
960
|
+
const owner = await this.resolveCachedEmailOwner(cachedEmailArg);
|
|
961
|
+
const status = this.mapMailSpoolStatus(cachedEmailArg, queueItem);
|
|
962
|
+
const cachedEmailWithTimestamps = cachedEmailArg as TCachedEmailWithTimestamps;
|
|
963
|
+
const createdAt = this.toTimestamp(cachedEmailWithTimestamps.createdAt) || Date.now();
|
|
964
|
+
const updatedAt = this.toTimestamp(cachedEmailWithTimestamps.updatedAt) || createdAt;
|
|
965
|
+
const nextAttemptAt = status === 'delivered' || status === 'failed'
|
|
966
|
+
? undefined
|
|
967
|
+
: this.toTimestamp(queueItem?.nextAttempt) || this.toTimestamp(routeData.smartMta?.nextAttempt) || this.toTimestamp(cachedEmailArg.nextAttempt);
|
|
968
|
+
const lastError = queueItem?.lastError || cachedEmailArg.lastError || routeData.smartMta?.lastError || undefined;
|
|
969
|
+
|
|
970
|
+
return {
|
|
971
|
+
spoolItem: {
|
|
972
|
+
id: cachedEmailArg.id,
|
|
973
|
+
owner,
|
|
974
|
+
direction: this.getCachedEmailSessionUsername(cachedEmailArg) ? 'outbound' : 'inbound',
|
|
975
|
+
status,
|
|
976
|
+
envelope: {
|
|
977
|
+
mailFrom: cachedEmailArg.from,
|
|
978
|
+
rcptTo: cachedEmailArg.to || [],
|
|
979
|
+
},
|
|
980
|
+
messageId: cachedEmailArg.messageId,
|
|
981
|
+
subject: cachedEmailArg.subject,
|
|
982
|
+
target: this.getCachedEmailSessionUsername(cachedEmailArg)
|
|
983
|
+
? { type: 'mtaRelay', label: 'SmartMTA outbound relay' }
|
|
984
|
+
: undefined,
|
|
985
|
+
attempts: Math.max(cachedEmailArg.attempts || 0, queueItem?.attempts || 0),
|
|
986
|
+
nextAttemptAt,
|
|
987
|
+
lastError,
|
|
988
|
+
createdAt,
|
|
989
|
+
updatedAt,
|
|
990
|
+
},
|
|
991
|
+
attempts: [],
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
private findQueueItemForCachedEmail(cachedEmailIdArg: string): TSmartMtaStatusQueueItem | undefined {
|
|
996
|
+
const emailServer = this.dcRouterRef.emailServer;
|
|
997
|
+
if (!emailServer?.getQueueItems) {
|
|
998
|
+
return undefined;
|
|
999
|
+
}
|
|
1000
|
+
return (emailServer.getQueueItems() as TSmartMtaStatusQueueItem[]).find((itemArg) => {
|
|
1001
|
+
return this.getQueueItemCachedEmailId(itemArg) === cachedEmailIdArg;
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
private getQueueItemCachedEmailId(itemArg: TSmartMtaStatusQueueItem): string | undefined {
|
|
1006
|
+
const headerName = 'x-dcrouter-cached-email-id';
|
|
1007
|
+
const headers = itemArg?.processingResult?.headers || itemArg?.processingResult?.email?.headers;
|
|
1008
|
+
if (!headers) {
|
|
1009
|
+
return undefined;
|
|
1010
|
+
}
|
|
1011
|
+
const matchingHeaderName = Object.keys(headers).find((keyArg) => keyArg.toLowerCase() === headerName);
|
|
1012
|
+
return matchingHeaderName ? headers[matchingHeaderName] : undefined;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private async resolveCachedEmailOwner(
|
|
1016
|
+
cachedEmailArg: CachedEmail,
|
|
1017
|
+
): Promise<plugins.servezoneInterfaces.data.IMailResourceOwner | undefined> {
|
|
1018
|
+
const username = this.getCachedEmailSessionUsername(cachedEmailArg);
|
|
1019
|
+
if (!username) {
|
|
1020
|
+
return undefined;
|
|
1021
|
+
}
|
|
1022
|
+
const identity = (await this.readStoredIdentities()).find((identityArg) => identityArg.smtp.username === username);
|
|
1023
|
+
return identity ? this.toMailOwner(identity.ownership) : undefined;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private cachedEmailBelongsToIdentity(
|
|
1027
|
+
cachedEmailArg: CachedEmail,
|
|
1028
|
+
identityArg: IStoredWorkAppMailIdentity,
|
|
1029
|
+
): boolean {
|
|
1030
|
+
return this.normalizeAddress(cachedEmailArg.from || '') === identityArg.address
|
|
1031
|
+
&& this.getCachedEmailSessionUsername(cachedEmailArg) === identityArg.smtp.username;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
private getCachedEmailSessionUsername(cachedEmailArg: CachedEmail): string | undefined {
|
|
1035
|
+
const routeData = this.parseCachedEmailRouteData(cachedEmailArg);
|
|
1036
|
+
const username = routeData.session?.user?.username;
|
|
1037
|
+
return typeof username === 'string' && username ? username : undefined;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
private parseCachedEmailRouteData(cachedEmailArg: CachedEmail): TCachedEmailRouteData {
|
|
1041
|
+
try {
|
|
1042
|
+
return cachedEmailArg.routeData ? JSON.parse(cachedEmailArg.routeData) : {};
|
|
1043
|
+
} catch {
|
|
1044
|
+
return {};
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
private mapMailSpoolStatus(
|
|
1049
|
+
cachedEmailArg: CachedEmail,
|
|
1050
|
+
queueItemArg?: TSmartMtaStatusQueueItem,
|
|
1051
|
+
): plugins.servezoneInterfaces.data.TMailSpoolStatus {
|
|
1052
|
+
if (!queueItemArg && cachedEmailArg.status === 'pending') {
|
|
1053
|
+
return 'accepted';
|
|
1054
|
+
}
|
|
1055
|
+
const status = queueItemArg?.status || cachedEmailArg.status;
|
|
1056
|
+
switch (status) {
|
|
1057
|
+
case 'processing':
|
|
1058
|
+
return 'delivering';
|
|
1059
|
+
case 'delivered':
|
|
1060
|
+
return 'delivered';
|
|
1061
|
+
case 'deferred':
|
|
1062
|
+
return 'deferred';
|
|
1063
|
+
case 'failed':
|
|
1064
|
+
return 'failed';
|
|
1065
|
+
case 'pending':
|
|
1066
|
+
case 'queued':
|
|
1067
|
+
default:
|
|
1068
|
+
return 'queued';
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
private toTimestamp(valueArg: unknown): number | undefined {
|
|
1073
|
+
if (!valueArg) {
|
|
1074
|
+
return undefined;
|
|
1075
|
+
}
|
|
1076
|
+
if (typeof valueArg === 'number' && Number.isFinite(valueArg)) {
|
|
1077
|
+
return valueArg;
|
|
1078
|
+
}
|
|
1079
|
+
const timestamp = valueArg instanceof Date
|
|
1080
|
+
? valueArg.getTime()
|
|
1081
|
+
: new Date(String(valueArg)).getTime();
|
|
1082
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
892
1085
|
private async authenticateMailCredential(
|
|
893
1086
|
authArg: plugins.servezoneInterfaces.requests.mail.IMailSubmissionRequestAuth,
|
|
894
1087
|
addressArg?: string,
|
|
@@ -196,6 +196,17 @@ export class GatewayClientHandler {
|
|
|
196
196
|
),
|
|
197
197
|
);
|
|
198
198
|
|
|
199
|
+
this.typedrouter.addTypedHandler(
|
|
200
|
+
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.gateway.IReq_GetGatewayClientRoutes>(
|
|
201
|
+
'getGatewayClientRoutes',
|
|
202
|
+
async (dataArg) => {
|
|
203
|
+
const auth = await this.requireAuth(dataArg, 'gateway-clients:read');
|
|
204
|
+
this.assertCapability(auth, 'readDnsRecords');
|
|
205
|
+
return { routes: this.listGatewayClientRoutes(auth, dataArg.gatewayClientId) };
|
|
206
|
+
},
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
|
|
199
210
|
|
|
200
211
|
this.typedrouter.addTypedHandler(
|
|
201
212
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.gateway.IReq_SyncGatewayClientRoute>(
|
|
@@ -339,6 +350,34 @@ export class GatewayClientHandler {
|
|
|
339
350
|
),
|
|
340
351
|
);
|
|
341
352
|
|
|
353
|
+
this.typedrouter.addTypedHandler(
|
|
354
|
+
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.mail.IReq_GetMailDeliveryStatus>(
|
|
355
|
+
'getMailDeliveryStatus',
|
|
356
|
+
async (dataArg) => {
|
|
357
|
+
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
|
358
|
+
if (!manager) {
|
|
359
|
+
return { attempts: [] };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const authData = dataArg.auth || {};
|
|
363
|
+
const mailSubmissionAuth = authData as plugins.servezoneInterfaces.requests.mail.IMailSubmissionRequestAuth;
|
|
364
|
+
if (mailSubmissionAuth.credentialId || mailSubmissionAuth.credentialSecret) {
|
|
365
|
+
return await manager.getMailDeliveryStatusForCredential(
|
|
366
|
+
mailSubmissionAuth,
|
|
367
|
+
dataArg.spoolItemId,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const auth = await this.requireAuth(authData, 'gateway-clients:read');
|
|
372
|
+
const status = await manager.getMailDeliveryStatus(dataArg.spoolItemId);
|
|
373
|
+
if (!this.isMailDeliveryStatusAllowed(auth, status)) {
|
|
374
|
+
return { attempts: [] };
|
|
375
|
+
}
|
|
376
|
+
return status;
|
|
377
|
+
},
|
|
378
|
+
),
|
|
379
|
+
);
|
|
380
|
+
|
|
342
381
|
this.typedrouter.addTypedHandler(
|
|
343
382
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.mail.IReq_RegisterServiceMailEndpoint>(
|
|
344
383
|
'registerServiceMailEndpoint',
|
|
@@ -429,13 +468,23 @@ export class GatewayClientHandler {
|
|
|
429
468
|
throw new plugins.typedrequest.TypedResponseError(`token capability missing: ${String(capability)}`);
|
|
430
469
|
}
|
|
431
470
|
|
|
432
|
-
private
|
|
433
|
-
|
|
434
|
-
|
|
471
|
+
private resolveGatewayClientRouteFilter(
|
|
472
|
+
auth: TAuthContext,
|
|
473
|
+
requestedId?: string,
|
|
474
|
+
): { gatewayClientId?: string; gatewayClientType?: string } {
|
|
475
|
+
const policy = auth.token?.policy;
|
|
476
|
+
if (policy?.role !== 'gatewayClient') return { gatewayClientId: requestedId };
|
|
477
|
+
const policyClient = policy.gatewayClient;
|
|
478
|
+
if (!policyClient) {
|
|
479
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token is missing gatewayClient binding');
|
|
480
|
+
}
|
|
435
481
|
if (requestedId && requestedId !== policyClient.id) {
|
|
436
482
|
throw new plugins.typedrequest.TypedResponseError('gateway client token cannot access another gateway client');
|
|
437
483
|
}
|
|
438
|
-
return
|
|
484
|
+
return {
|
|
485
|
+
gatewayClientId: policyClient.id,
|
|
486
|
+
gatewayClientType: policyClient.type,
|
|
487
|
+
};
|
|
439
488
|
}
|
|
440
489
|
|
|
441
490
|
private resolveGatewayClientOwnership(
|
|
@@ -588,6 +637,22 @@ export class GatewayClientHandler {
|
|
|
588
637
|
});
|
|
589
638
|
}
|
|
590
639
|
|
|
640
|
+
private isMailDeliveryStatusAllowed(
|
|
641
|
+
auth: TAuthContext,
|
|
642
|
+
statusArg: plugins.servezoneInterfaces.data.IMailDeliveryStatus,
|
|
643
|
+
): boolean {
|
|
644
|
+
const policy = auth.token?.policy;
|
|
645
|
+
if (!policy || policy.role !== 'gatewayClient') {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
const owner = statusArg.spoolItem?.owner;
|
|
649
|
+
if (!owner || !policy.gatewayClient) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
return owner.gatewayClientType === policy.gatewayClient.type
|
|
653
|
+
&& owner.gatewayClientId === policy.gatewayClient.id;
|
|
654
|
+
}
|
|
655
|
+
|
|
591
656
|
private async filterWorkAppMailBindingsAllowed(
|
|
592
657
|
auth: TAuthContext,
|
|
593
658
|
managerArg: any,
|
|
@@ -665,25 +730,85 @@ export class GatewayClientHandler {
|
|
|
665
730
|
return [];
|
|
666
731
|
}
|
|
667
732
|
|
|
668
|
-
private getOwnedRoutes(gatewayClientId?: string): interfaces.data.IMergedRoute[] {
|
|
733
|
+
private getOwnedRoutes(routeFilter: { gatewayClientId?: string; gatewayClientType?: string }): interfaces.data.IMergedRoute[] {
|
|
669
734
|
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
670
735
|
if (!manager) return [];
|
|
671
736
|
return manager.getMergedRoutes().routes.filter((route) => {
|
|
672
737
|
const metadata = route.metadata;
|
|
673
738
|
if (!metadata) return false;
|
|
674
739
|
if (metadata.ownerType !== 'gatewayClient') return false;
|
|
675
|
-
|
|
740
|
+
if (routeFilter.gatewayClientId && metadata.gatewayClientId !== routeFilter.gatewayClientId) return false;
|
|
741
|
+
if (routeFilter.gatewayClientType && (metadata.gatewayClientType || 'custom') !== routeFilter.gatewayClientType) return false;
|
|
742
|
+
return true;
|
|
676
743
|
});
|
|
677
744
|
}
|
|
678
745
|
|
|
746
|
+
private listGatewayClientRoutes(
|
|
747
|
+
auth: TAuthContext,
|
|
748
|
+
requestedGatewayClientId?: string,
|
|
749
|
+
): plugins.servezoneInterfaces.data.IGatewayClientRoute[] {
|
|
750
|
+
const routeFilter = this.resolveGatewayClientRouteFilter(auth, requestedGatewayClientId);
|
|
751
|
+
return this.getOwnedRoutes(routeFilter)
|
|
752
|
+
.map((routeArg) => this.toGatewayClientRoute(routeArg))
|
|
753
|
+
.filter((routeArg): routeArg is plugins.servezoneInterfaces.data.IGatewayClientRoute => {
|
|
754
|
+
if (!routeArg) return false;
|
|
755
|
+
if (auth.token?.policy?.role !== 'gatewayClient') return true;
|
|
756
|
+
return !routeArg.ownership.hostname
|
|
757
|
+
|| this.matchesHostnamePatterns(routeArg.ownership.hostname, auth.token.policy.hostnamePatterns || []);
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
private toGatewayClientRoute(
|
|
762
|
+
routeArg: interfaces.data.IMergedRoute,
|
|
763
|
+
): plugins.servezoneInterfaces.data.IGatewayClientRoute | null {
|
|
764
|
+
const metadata = routeArg.metadata;
|
|
765
|
+
if (!metadata || metadata.ownerType !== 'gatewayClient') return null;
|
|
766
|
+
const ownership = this.toGatewayClientOwnership(routeArg);
|
|
767
|
+
if (!ownership.hostname && !ownership.routeRef) return null;
|
|
768
|
+
const route = structuredClone(routeArg.route) as any;
|
|
769
|
+
delete route.security;
|
|
770
|
+
return {
|
|
771
|
+
id: routeArg.id,
|
|
772
|
+
name: route.name || routeArg.id,
|
|
773
|
+
enabled: routeArg.enabled,
|
|
774
|
+
ownership,
|
|
775
|
+
route,
|
|
776
|
+
sourceProfileRef: metadata.sourceBindings?.find((bindingArg) => bindingArg.sourceProfileRef)?.sourceProfileRef,
|
|
777
|
+
managePath: `/routes/${routeArg.id}`,
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private toGatewayClientOwnership(
|
|
782
|
+
routeArg: interfaces.data.IMergedRoute,
|
|
783
|
+
): plugins.servezoneInterfaces.data.IGatewayClientOwnership {
|
|
784
|
+
const metadata = routeArg.metadata!;
|
|
785
|
+
const ownership: plugins.servezoneInterfaces.data.IGatewayClientOwnership = {
|
|
786
|
+
gatewayClientType: metadata.gatewayClientType || 'custom',
|
|
787
|
+
gatewayClientId: metadata.gatewayClientId || '',
|
|
788
|
+
appId: metadata.gatewayClientAppId || '',
|
|
789
|
+
};
|
|
790
|
+
const prefix = `${ownership.gatewayClientType}:${ownership.gatewayClientId}:${ownership.appId}:`;
|
|
791
|
+
const routeKey = metadata.externalKey?.startsWith(prefix)
|
|
792
|
+
? metadata.externalKey.slice(prefix.length)
|
|
793
|
+
: '';
|
|
794
|
+
if (routeKey.startsWith('route:')) {
|
|
795
|
+
ownership.routeRef = routeKey.slice('route:'.length);
|
|
796
|
+
} else if (routeKey) {
|
|
797
|
+
ownership.hostname = routeKey;
|
|
798
|
+
} else {
|
|
799
|
+
ownership.hostname = this.getRouteHostnames(routeArg.route)[0];
|
|
800
|
+
}
|
|
801
|
+
return ownership;
|
|
802
|
+
}
|
|
803
|
+
|
|
679
804
|
private async listGatewayClientDomains(
|
|
680
805
|
auth: TAuthContext,
|
|
681
806
|
requestedGatewayClientId?: string,
|
|
682
807
|
): Promise<plugins.servezoneInterfaces.data.IGatewayDomain[]> {
|
|
683
808
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
684
809
|
if (!dnsManager) return [];
|
|
685
|
-
const
|
|
686
|
-
const ownedRoutes = this.getOwnedRoutes(
|
|
810
|
+
const routeFilter = this.resolveGatewayClientRouteFilter(auth, requestedGatewayClientId);
|
|
811
|
+
const ownedRoutes = this.getOwnedRoutes(routeFilter);
|
|
687
812
|
const routeHostnames = ownedRoutes.flatMap((route) => this.getRouteHostnames(route.route));
|
|
688
813
|
const docs = await dnsManager.listDomains();
|
|
689
814
|
|
|
@@ -716,8 +841,8 @@ export class GatewayClientHandler {
|
|
|
716
841
|
): Promise<plugins.servezoneInterfaces.data.IGatewayDnsRecord[]> {
|
|
717
842
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
718
843
|
if (!dnsManager) return [];
|
|
719
|
-
const
|
|
720
|
-
const ownedRoutes = this.getOwnedRoutes(
|
|
844
|
+
const routeFilter = this.resolveGatewayClientRouteFilter(auth, requestedGatewayClientId);
|
|
845
|
+
const ownedRoutes = this.getOwnedRoutes(routeFilter);
|
|
721
846
|
const domains = await dnsManager.listDomains();
|
|
722
847
|
const records: plugins.servezoneInterfaces.data.IGatewayDnsRecord[] = [];
|
|
723
848
|
|
package/ts/readme.md
CHANGED
|
@@ -68,7 +68,7 @@ await router.start();
|
|
|
68
68
|
- The DB is enabled by default and uses an embedded local database when no external MongoDB URL is provided.
|
|
69
69
|
- System routes from config, email, and DNS are persisted with stable ownership and are toggle-only.
|
|
70
70
|
- API-created routes are the only routes intended for full CRUD from the dashboard or client SDK.
|
|
71
|
-
- Gateway-client mail bindings can create inbound `smtpForward` routes and managed SMTP users. Managed users are restricted to their exact claimed address for envelope and header `From
|
|
71
|
+
- Gateway-client mail bindings can create inbound `smtpForward` routes and managed SMTP users. Managed users are restricted to their exact claimed address for envelope and header `From`, and service-mail credentials can query their own delivery spool status after enqueueing mail.
|
|
72
72
|
- Qualifying HTTPS forward routes on port `443` get HTTP/3 augmentation by default.
|
|
73
73
|
- The published package exposes the `dcrouter` npm bin through `./cli.js`; `runCli()` is the supported code-level bootstrap entrypoint.
|
|
74
74
|
|