@serve.zone/dcrouter 13.25.0 → 13.27.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/.smartconfig.json +3 -11
- package/dist_serve/bundle.js +4046 -3552
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +2 -1
- package/dist_ts/classes.dcrouter.js +6 -4
- package/dist_ts/config/classes.api-token-manager.d.ts +4 -2
- package/dist_ts/config/classes.api-token-manager.js +68 -6
- package/dist_ts/config/classes.route-config-manager.d.ts +1 -0
- package/dist_ts/config/classes.route-config-manager.js +38 -1
- package/dist_ts/db/documents/classes.api-token.doc.d.ts +2 -1
- package/dist_ts/db/documents/classes.api-token.doc.js +8 -2
- package/dist_ts/email/classes.email-domain.manager.d.ts +4 -1
- package/dist_ts/email/classes.email-domain.manager.js +30 -1
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +35 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +273 -0
- package/dist_ts/email/index.d.ts +1 -0
- package/dist_ts/email/index.js +2 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/admin.handler.js +9 -4
- package/dist_ts/opsserver/handlers/api-token.handler.js +2 -2
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +4 -0
- package/dist_ts/opsserver/handlers/certificate.handler.js +41 -11
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/workhoster.handler.d.ts +26 -0
- package/dist_ts/opsserver/handlers/workhoster.handler.js +402 -0
- package/dist_ts_apiclient/classes.dcrouterapiclient.d.ts +2 -0
- package/dist_ts_apiclient/classes.dcrouterapiclient.js +4 -1
- package/dist_ts_apiclient/classes.workhoster.d.ts +14 -0
- package/dist_ts_apiclient/classes.workhoster.js +29 -0
- package/dist_ts_apiclient/index.d.ts +1 -0
- package/dist_ts_apiclient/index.js +2 -1
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +38 -1
- package/dist_ts_interfaces/data/workhoster.d.ts +131 -0
- package/dist_ts_interfaces/data/workhoster.js +2 -0
- package/dist_ts_interfaces/requests/api-tokens.d.ts +2 -1
- package/dist_ts_interfaces/requests/certificate.d.ts +12 -6
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/workhoster.d.ts +98 -0
- package/dist_ts_interfaces/requests/workhoster.js +2 -0
- package/dist_ts_migrations/index.js +8 -2
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate.d.ts +1 -1
- package/dist_ts_web/appstate.js +3 -2
- package/dist_ts_web/elements/access/ops-view-apitokens.d.ts +1 -0
- package/dist_ts_web/elements/access/ops-view-apitokens.js +58 -3
- package/package.json +24 -23
- package/readme.md +108 -128
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +5 -3
- package/ts/config/classes.api-token-manager.ts +73 -4
- package/ts/config/classes.route-config-manager.ts +37 -0
- package/ts/db/documents/classes.api-token.doc.ts +4 -1
- package/ts/email/classes.email-domain.manager.ts +33 -1
- package/ts/email/classes.workapp-mail-manager.ts +343 -0
- package/ts/email/index.ts +1 -0
- package/ts/opsserver/classes.opsserver.ts +3 -1
- package/ts/opsserver/handlers/admin.handler.ts +11 -4
- package/ts/opsserver/handlers/api-token.handler.ts +1 -0
- package/ts/opsserver/handlers/certificate.handler.ts +45 -12
- package/ts/opsserver/handlers/index.ts +2 -1
- package/ts/opsserver/handlers/workhoster.handler.ts +490 -0
- package/ts/readme.md +2 -2
- package/ts_apiclient/classes.dcrouterapiclient.ts +3 -0
- package/ts_apiclient/classes.workhoster.ts +49 -0
- package/ts_apiclient/index.ts +1 -0
- package/ts_apiclient/readme.md +54 -44
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate.ts +7 -1
- package/ts_web/elements/access/ops-view-apitokens.ts +58 -3
- package/ts_web/readme.md +36 -19
|
@@ -38,6 +38,7 @@ export class OpsServer {
|
|
|
38
38
|
private dnsRecordHandler!: handlers.DnsRecordHandler;
|
|
39
39
|
private acmeConfigHandler!: handlers.AcmeConfigHandler;
|
|
40
40
|
private emailDomainHandler!: handlers.EmailDomainHandler;
|
|
41
|
+
private workHosterHandler!: handlers.WorkHosterHandler;
|
|
41
42
|
|
|
42
43
|
constructor(dcRouterRefArg: DcRouter) {
|
|
43
44
|
this.dcRouterRef = dcRouterRefArg;
|
|
@@ -106,6 +107,7 @@ export class OpsServer {
|
|
|
106
107
|
this.dnsRecordHandler = new handlers.DnsRecordHandler(this);
|
|
107
108
|
this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
|
|
108
109
|
this.emailDomainHandler = new handlers.EmailDomainHandler(this);
|
|
110
|
+
this.workHosterHandler = new handlers.WorkHosterHandler(this);
|
|
109
111
|
|
|
110
112
|
console.log('✅ OpsServer TypedRequest handlers initialized');
|
|
111
113
|
}
|
|
@@ -119,4 +121,4 @@ export class OpsServer {
|
|
|
119
121
|
await this.server.stop();
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
|
-
}
|
|
124
|
+
}
|
|
@@ -43,14 +43,21 @@ export class AdminHandler {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
private initializeDefaultUsers(): void {
|
|
46
|
-
|
|
46
|
+
const username = process.env.DCROUTER_ADMIN_USERNAME || 'admin';
|
|
47
|
+
const configuredPassword = process.env.DCROUTER_ADMIN_PASSWORD;
|
|
48
|
+
const password = configuredPassword || plugins.crypto.randomBytes(24).toString('base64url');
|
|
49
|
+
|
|
47
50
|
const adminId = plugins.uuid.v4();
|
|
48
51
|
this.users.set(adminId, {
|
|
49
52
|
id: adminId,
|
|
50
|
-
username
|
|
51
|
-
password
|
|
53
|
+
username,
|
|
54
|
+
password,
|
|
52
55
|
role: 'admin',
|
|
53
56
|
});
|
|
57
|
+
|
|
58
|
+
if (!configuredPassword) {
|
|
59
|
+
console.warn(`DCRouter generated one-time admin password for ${username}: ${password}`);
|
|
60
|
+
}
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
/**
|
|
@@ -249,4 +256,4 @@ export class AdminHandler {
|
|
|
249
256
|
name: 'adminIdentityGuard',
|
|
250
257
|
}
|
|
251
258
|
);
|
|
252
|
-
}
|
|
259
|
+
}
|
|
@@ -26,21 +26,51 @@ export function deriveCertDomainName(domain: string): string | undefined {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export class CertificateHandler {
|
|
29
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
30
|
+
|
|
29
31
|
constructor(private opsServerRef: OpsServer) {
|
|
32
|
+
this.opsServerRef.typedrouter?.addTypedRouter(this.typedrouter);
|
|
30
33
|
this.registerHandlers();
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
private async requireAuth(
|
|
37
|
+
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
38
|
+
requiredScope?: interfaces.data.TApiTokenScope,
|
|
39
|
+
): Promise<string> {
|
|
40
|
+
if (request.identity?.jwt) {
|
|
41
|
+
try {
|
|
42
|
+
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
|
|
43
|
+
identity: request.identity,
|
|
44
|
+
});
|
|
45
|
+
if (isAdmin) return request.identity.userId;
|
|
46
|
+
} catch { /* fall through */ }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (request.apiToken) {
|
|
50
|
+
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
51
|
+
if (tokenManager) {
|
|
52
|
+
const token = await tokenManager.validateToken(request.apiToken);
|
|
53
|
+
if (token) {
|
|
54
|
+
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
|
55
|
+
return token.createdBy;
|
|
56
|
+
}
|
|
57
|
+
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
36
61
|
|
|
37
|
-
|
|
62
|
+
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private registerHandlers(): void {
|
|
66
|
+
const router = this.typedrouter;
|
|
38
67
|
|
|
39
68
|
// Get Certificate Overview
|
|
40
|
-
|
|
69
|
+
router.addTypedHandler(
|
|
41
70
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetCertificateOverview>(
|
|
42
71
|
'getCertificateOverview',
|
|
43
72
|
async (dataArg) => {
|
|
73
|
+
await this.requireAuth(dataArg, 'certificates:read');
|
|
44
74
|
const certificates = await this.buildCertificateOverview();
|
|
45
75
|
const summary = this.buildSummary(certificates);
|
|
46
76
|
return { certificates, summary };
|
|
@@ -48,53 +78,56 @@ export class CertificateHandler {
|
|
|
48
78
|
)
|
|
49
79
|
);
|
|
50
80
|
|
|
51
|
-
// ---- Write endpoints (adminRouter — admin identity required via middleware) ----
|
|
52
|
-
|
|
53
81
|
// Legacy route-based reprovision (backward compat)
|
|
54
|
-
|
|
82
|
+
router.addTypedHandler(
|
|
55
83
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificate>(
|
|
56
84
|
'reprovisionCertificate',
|
|
57
85
|
async (dataArg) => {
|
|
86
|
+
await this.requireAuth(dataArg, 'certificates:write');
|
|
58
87
|
return this.reprovisionCertificateByRoute(dataArg.routeName);
|
|
59
88
|
}
|
|
60
89
|
)
|
|
61
90
|
);
|
|
62
91
|
|
|
63
92
|
// Domain-based reprovision (preferred)
|
|
64
|
-
|
|
93
|
+
router.addTypedHandler(
|
|
65
94
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ReprovisionCertificateDomain>(
|
|
66
95
|
'reprovisionCertificateDomain',
|
|
67
96
|
async (dataArg) => {
|
|
97
|
+
await this.requireAuth(dataArg, 'certificates:write');
|
|
68
98
|
return this.reprovisionCertificateDomain(dataArg.domain, dataArg.forceRenew);
|
|
69
99
|
}
|
|
70
100
|
)
|
|
71
101
|
);
|
|
72
102
|
|
|
73
103
|
// Delete certificate
|
|
74
|
-
|
|
104
|
+
router.addTypedHandler(
|
|
75
105
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_DeleteCertificate>(
|
|
76
106
|
'deleteCertificate',
|
|
77
107
|
async (dataArg) => {
|
|
108
|
+
await this.requireAuth(dataArg, 'certificates:write');
|
|
78
109
|
return this.deleteCertificate(dataArg.domain);
|
|
79
110
|
}
|
|
80
111
|
)
|
|
81
112
|
);
|
|
82
113
|
|
|
83
114
|
// Export certificate
|
|
84
|
-
|
|
115
|
+
router.addTypedHandler(
|
|
85
116
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ExportCertificate>(
|
|
86
117
|
'exportCertificate',
|
|
87
118
|
async (dataArg) => {
|
|
119
|
+
await this.requireAuth(dataArg, 'certificates:read');
|
|
88
120
|
return this.exportCertificate(dataArg.domain);
|
|
89
121
|
}
|
|
90
122
|
)
|
|
91
123
|
);
|
|
92
124
|
|
|
93
125
|
// Import certificate
|
|
94
|
-
|
|
126
|
+
router.addTypedHandler(
|
|
95
127
|
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ImportCertificate>(
|
|
96
128
|
'importCertificate',
|
|
97
129
|
async (dataArg) => {
|
|
130
|
+
await this.requireAuth(dataArg, 'certificates:write');
|
|
98
131
|
return this.importCertificate(dataArg.cert);
|
|
99
132
|
}
|
|
100
133
|
)
|
|
@@ -18,4 +18,5 @@ export * from './dns-provider.handler.js';
|
|
|
18
18
|
export * from './domain.handler.js';
|
|
19
19
|
export * from './dns-record.handler.js';
|
|
20
20
|
export * from './acme-config.handler.js';
|
|
21
|
-
export * from './email-domain.handler.js';
|
|
21
|
+
export * from './email-domain.handler.js';
|
|
22
|
+
export * from './workhoster.handler.js';
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
|
|
5
|
+
type TAuthContext = {
|
|
6
|
+
userId: string;
|
|
7
|
+
isAdmin: boolean;
|
|
8
|
+
token?: interfaces.data.IStoredApiToken;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class WorkHosterHandler {
|
|
12
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
13
|
+
|
|
14
|
+
constructor(private opsServerRef: OpsServer) {
|
|
15
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
16
|
+
this.registerHandlers();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private async requireAuth(
|
|
20
|
+
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
21
|
+
requiredScope?: interfaces.data.TApiTokenScope,
|
|
22
|
+
): Promise<TAuthContext> {
|
|
23
|
+
if (request.identity?.jwt) {
|
|
24
|
+
try {
|
|
25
|
+
const isAdmin = await this.opsServerRef.adminHandler.adminIdentityGuard.exec({
|
|
26
|
+
identity: request.identity,
|
|
27
|
+
});
|
|
28
|
+
if (isAdmin) return { userId: request.identity.userId, isAdmin: true };
|
|
29
|
+
} catch { /* fall through */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (request.apiToken) {
|
|
33
|
+
const tokenManager = this.opsServerRef.dcRouterRef.apiTokenManager;
|
|
34
|
+
if (tokenManager) {
|
|
35
|
+
const token = await tokenManager.validateToken(request.apiToken);
|
|
36
|
+
if (token) {
|
|
37
|
+
if (!requiredScope || tokenManager.hasScope(token, requiredScope)) {
|
|
38
|
+
return { userId: token.createdBy, isAdmin: token.policy?.role === 'admin', token };
|
|
39
|
+
}
|
|
40
|
+
throw new plugins.typedrequest.TypedResponseError('insufficient scope');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw new plugins.typedrequest.TypedResponseError('unauthorized');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private registerHandlers(): void {
|
|
49
|
+
this.typedrouter.addTypedHandler(
|
|
50
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayCapabilities>(
|
|
51
|
+
'getGatewayCapabilities',
|
|
52
|
+
async (dataArg) => {
|
|
53
|
+
await this.requireAuth(dataArg, 'gateway-clients:read');
|
|
54
|
+
return { capabilities: this.getGatewayCapabilities() };
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
this.typedrouter.addTypedHandler(
|
|
60
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayClientDomains>(
|
|
61
|
+
'getGatewayClientDomains',
|
|
62
|
+
async (dataArg) => {
|
|
63
|
+
const auth = await this.requireAuth(dataArg, 'gateway-clients:read');
|
|
64
|
+
this.assertCapability(auth, 'readDomains');
|
|
65
|
+
return { domains: await this.listGatewayClientDomains(auth, dataArg.gatewayClientId) };
|
|
66
|
+
},
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
this.typedrouter.addTypedHandler(
|
|
71
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetGatewayClientDnsRecords>(
|
|
72
|
+
'getGatewayClientDnsRecords',
|
|
73
|
+
async (dataArg) => {
|
|
74
|
+
const auth = await this.requireAuth(dataArg, 'gateway-clients:read');
|
|
75
|
+
this.assertCapability(auth, 'readDnsRecords');
|
|
76
|
+
return { records: await this.listGatewayClientDnsRecords(auth, dataArg.gatewayClientId) };
|
|
77
|
+
},
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
this.typedrouter.addTypedHandler(
|
|
82
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetWorkHosterDomains>(
|
|
83
|
+
'getWorkHosterDomains',
|
|
84
|
+
async (dataArg) => {
|
|
85
|
+
const auth = await this.requireAuth(dataArg, 'workhosters:read');
|
|
86
|
+
this.assertCapability(auth, 'readDomains');
|
|
87
|
+
return { domains: await this.listGatewayClientDomains(auth) };
|
|
88
|
+
},
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
this.typedrouter.addTypedHandler(
|
|
93
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncGatewayClientRoute>(
|
|
94
|
+
'syncGatewayClientRoute',
|
|
95
|
+
async (dataArg) => {
|
|
96
|
+
const auth = await this.requireAuth(dataArg, 'gateway-clients:write');
|
|
97
|
+
this.assertCapability(auth, 'syncRoutes');
|
|
98
|
+
return await this.syncGatewayClientRoute(auth, dataArg.ownership, dataArg.route, dataArg.enabled, dataArg.delete);
|
|
99
|
+
},
|
|
100
|
+
),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
this.typedrouter.addTypedHandler(
|
|
104
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncWorkAppRoute>(
|
|
105
|
+
'syncWorkAppRoute',
|
|
106
|
+
async (dataArg) => {
|
|
107
|
+
const auth = await this.requireAuth(dataArg, 'workhosters:write');
|
|
108
|
+
this.assertCapability(auth, 'syncRoutes');
|
|
109
|
+
const ownership: interfaces.data.IGatewayClientOwnership = {
|
|
110
|
+
gatewayClientType: dataArg.ownership.workHosterType,
|
|
111
|
+
gatewayClientId: dataArg.ownership.workHosterId,
|
|
112
|
+
appId: dataArg.ownership.workAppId,
|
|
113
|
+
hostname: dataArg.ownership.hostname,
|
|
114
|
+
};
|
|
115
|
+
return await this.syncGatewayClientRoute(auth, ownership, dataArg.route, dataArg.enabled, dataArg.delete);
|
|
116
|
+
},
|
|
117
|
+
),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
this.typedrouter.addTypedHandler(
|
|
121
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetWorkAppMailIdentities>(
|
|
122
|
+
'getWorkAppMailIdentities',
|
|
123
|
+
async (dataArg) => {
|
|
124
|
+
await this.requireAuth(dataArg, 'workhosters:read');
|
|
125
|
+
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
|
126
|
+
if (!manager) return { identities: [] };
|
|
127
|
+
return { identities: await manager.listMailIdentities(dataArg.ownership) };
|
|
128
|
+
},
|
|
129
|
+
),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
this.typedrouter.addTypedHandler(
|
|
133
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SyncWorkAppMailIdentity>(
|
|
134
|
+
'syncWorkAppMailIdentity',
|
|
135
|
+
async (dataArg) => {
|
|
136
|
+
const auth = await this.requireAuth(dataArg, 'workhosters:write');
|
|
137
|
+
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
|
138
|
+
if (!manager) {
|
|
139
|
+
return { success: false, message: 'WorkApp mail manager not initialized' };
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return await manager.syncMailIdentity(dataArg, auth.userId);
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return { success: false, message: (error as Error).message };
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private getGatewayCapabilities(): interfaces.data.IGatewayCapabilities {
|
|
152
|
+
const dcRouter = this.opsServerRef.dcRouterRef;
|
|
153
|
+
return {
|
|
154
|
+
routes: {
|
|
155
|
+
read: Boolean(dcRouter.routeConfigManager),
|
|
156
|
+
write: Boolean(dcRouter.routeConfigManager),
|
|
157
|
+
idempotentSync: Boolean(dcRouter.routeConfigManager),
|
|
158
|
+
},
|
|
159
|
+
domains: {
|
|
160
|
+
read: Boolean(dcRouter.dnsManager),
|
|
161
|
+
write: Boolean(dcRouter.dnsManager),
|
|
162
|
+
},
|
|
163
|
+
certificates: {
|
|
164
|
+
read: Boolean(dcRouter.smartProxy),
|
|
165
|
+
export: Boolean(dcRouter.smartProxy),
|
|
166
|
+
forceRenew: Boolean(dcRouter.smartProxy),
|
|
167
|
+
},
|
|
168
|
+
email: {
|
|
169
|
+
domains: Boolean(dcRouter.emailDomainManager),
|
|
170
|
+
inbound: Boolean(dcRouter.emailServer),
|
|
171
|
+
outbound: Boolean(dcRouter.emailServer),
|
|
172
|
+
},
|
|
173
|
+
remoteIngress: {
|
|
174
|
+
enabled: Boolean(dcRouter.options.remoteIngressConfig?.enabled),
|
|
175
|
+
},
|
|
176
|
+
dns: {
|
|
177
|
+
authoritative: Boolean(dcRouter.options.dnsScopes?.length),
|
|
178
|
+
providerManaged: Boolean(dcRouter.dnsManager),
|
|
179
|
+
},
|
|
180
|
+
http3: {
|
|
181
|
+
enabled: dcRouter.options.http3?.enabled !== false,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private buildExternalKey(ownership: interfaces.data.IWorkAppRouteOwnership): string {
|
|
187
|
+
return [
|
|
188
|
+
ownership.workHosterType,
|
|
189
|
+
ownership.workHosterId,
|
|
190
|
+
ownership.workAppId,
|
|
191
|
+
ownership.hostname,
|
|
192
|
+
].map((part) => part.trim()).join(':');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private assertCapability(
|
|
196
|
+
auth: TAuthContext,
|
|
197
|
+
capability: keyof NonNullable<interfaces.data.IApiTokenPolicy['capabilities']>,
|
|
198
|
+
): void {
|
|
199
|
+
if (auth.isAdmin) return;
|
|
200
|
+
const policy = auth.token?.policy;
|
|
201
|
+
if (!policy || policy.role !== 'gatewayClient') return;
|
|
202
|
+
if (policy.capabilities?.[capability] === true) return;
|
|
203
|
+
throw new plugins.typedrequest.TypedResponseError(`token capability missing: ${capability}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private resolveGatewayClientId(auth: TAuthContext, requestedId?: string): string | undefined {
|
|
207
|
+
const policyClient = auth.token?.policy?.gatewayClient;
|
|
208
|
+
if (!policyClient) return requestedId;
|
|
209
|
+
if (requestedId && requestedId !== policyClient.id) {
|
|
210
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token cannot access another gateway client');
|
|
211
|
+
}
|
|
212
|
+
return policyClient.id;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private assertGatewayClientOwnership(auth: TAuthContext, ownership: interfaces.data.IGatewayClientOwnership): void {
|
|
216
|
+
const policy = auth.token?.policy;
|
|
217
|
+
if (!policy || policy.role !== 'gatewayClient') return;
|
|
218
|
+
if (!policy.gatewayClient) {
|
|
219
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token is missing gatewayClient binding');
|
|
220
|
+
}
|
|
221
|
+
if (ownership.gatewayClientType !== policy.gatewayClient.type || ownership.gatewayClientId !== policy.gatewayClient.id) {
|
|
222
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token cannot act for this ownership');
|
|
223
|
+
}
|
|
224
|
+
if (!this.matchesHostnamePatterns(ownership.hostname, policy.hostnamePatterns || [])) {
|
|
225
|
+
throw new plugins.typedrequest.TypedResponseError('hostname is outside token policy');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private assertRouteTargetsAllowed(auth: TAuthContext, route?: interfaces.data.IDcRouterRouteConfig): void {
|
|
230
|
+
const policy = auth.token?.policy;
|
|
231
|
+
if (!policy || policy.role !== 'gatewayClient' || !route) return;
|
|
232
|
+
const allowedTargets = policy.allowedRouteTargets || [];
|
|
233
|
+
if (allowedTargets.length === 0) {
|
|
234
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token has no allowed route targets');
|
|
235
|
+
}
|
|
236
|
+
const targets = ((route.action as any)?.targets || []) as Array<{ host?: string; port?: number }>;
|
|
237
|
+
for (const target of targets) {
|
|
238
|
+
const host = String(target.host || '').trim().toLowerCase();
|
|
239
|
+
const port = Number(target.port);
|
|
240
|
+
const allowed = allowedTargets.some((allowedTarget) => {
|
|
241
|
+
return allowedTarget.host.trim().toLowerCase() === host && allowedTarget.ports.includes(port);
|
|
242
|
+
});
|
|
243
|
+
if (!allowed) {
|
|
244
|
+
throw new plugins.typedrequest.TypedResponseError(`route target is outside token policy: ${host}:${port}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private matchesHostnamePatterns(hostname: string, patterns: string[]): boolean {
|
|
250
|
+
const normalizedHostname = hostname.trim().toLowerCase();
|
|
251
|
+
if (!normalizedHostname) return false;
|
|
252
|
+
for (const pattern of patterns) {
|
|
253
|
+
const normalizedPattern = pattern.trim().toLowerCase();
|
|
254
|
+
if (!normalizedPattern) continue;
|
|
255
|
+
if (normalizedPattern === normalizedHostname) return true;
|
|
256
|
+
if (normalizedPattern.startsWith('*.')) {
|
|
257
|
+
const suffix = normalizedPattern.slice(2);
|
|
258
|
+
if (!normalizedHostname.endsWith(`.${suffix}`)) continue;
|
|
259
|
+
const prefix = normalizedHostname.slice(0, -(suffix.length + 1));
|
|
260
|
+
if (prefix && !prefix.includes('.')) return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private getRouteHostnames(route: interfaces.data.IDcRouterRouteConfig): string[] {
|
|
267
|
+
const domains = (route.match as any)?.domains;
|
|
268
|
+
if (Array.isArray(domains)) {
|
|
269
|
+
return domains.map((domain) => String(domain).trim().toLowerCase()).filter(Boolean);
|
|
270
|
+
}
|
|
271
|
+
if (typeof domains === 'string') {
|
|
272
|
+
return domains.split(',').map((domain) => domain.trim().toLowerCase()).filter(Boolean);
|
|
273
|
+
}
|
|
274
|
+
return [];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private getOwnedRoutes(gatewayClientId?: string): interfaces.data.IMergedRoute[] {
|
|
278
|
+
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
279
|
+
if (!manager) return [];
|
|
280
|
+
return manager.getMergedRoutes().routes.filter((route) => {
|
|
281
|
+
const metadata = route.metadata;
|
|
282
|
+
if (!metadata) return false;
|
|
283
|
+
const ownerType = metadata.ownerType;
|
|
284
|
+
const isGatewayOwned = ownerType === 'gatewayClient' || ownerType === 'workhoster';
|
|
285
|
+
if (!isGatewayOwned) return false;
|
|
286
|
+
const routeGatewayClientId = metadata.gatewayClientId || metadata.workHosterId;
|
|
287
|
+
return gatewayClientId ? routeGatewayClientId === gatewayClientId : true;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private async listGatewayClientDomains(
|
|
292
|
+
auth: TAuthContext,
|
|
293
|
+
requestedGatewayClientId?: string,
|
|
294
|
+
): Promise<interfaces.data.IGatewayClientDomain[]> {
|
|
295
|
+
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
296
|
+
if (!dnsManager) return [];
|
|
297
|
+
const gatewayClientId = this.resolveGatewayClientId(auth, requestedGatewayClientId);
|
|
298
|
+
const ownedRoutes = this.getOwnedRoutes(gatewayClientId);
|
|
299
|
+
const routeHostnames = ownedRoutes.flatMap((route) => this.getRouteHostnames(route.route));
|
|
300
|
+
const docs = await dnsManager.listDomains();
|
|
301
|
+
|
|
302
|
+
return docs
|
|
303
|
+
.filter((domainDoc) => {
|
|
304
|
+
if (!auth.token?.policy || auth.token.policy.role !== 'gatewayClient') return true;
|
|
305
|
+
return routeHostnames.some((hostname) => this.isHostnameInDomain(hostname, domainDoc.name));
|
|
306
|
+
})
|
|
307
|
+
.map((domainDoc) => {
|
|
308
|
+
const domain = dnsManager.toPublicDomain(domainDoc);
|
|
309
|
+
const canManageDnsRecords = domain.source === 'dcrouter' || Boolean(domain.providerId);
|
|
310
|
+
const serviceCount = routeHostnames.filter((hostname) => this.isHostnameInDomain(hostname, domain.name)).length;
|
|
311
|
+
return {
|
|
312
|
+
...domain,
|
|
313
|
+
serviceCount,
|
|
314
|
+
managePath: `/domains/${domain.id}`,
|
|
315
|
+
capabilities: {
|
|
316
|
+
canCreateSubdomains: canManageDnsRecords,
|
|
317
|
+
canManageDnsRecords,
|
|
318
|
+
canIssueCertificates: Boolean(this.opsServerRef.dcRouterRef.smartProxy),
|
|
319
|
+
canHostEmail: Boolean(this.opsServerRef.dcRouterRef.emailDomainManager),
|
|
320
|
+
},
|
|
321
|
+
} satisfies interfaces.data.IGatewayClientDomain;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async listGatewayClientDnsRecords(
|
|
326
|
+
auth: TAuthContext,
|
|
327
|
+
requestedGatewayClientId?: string,
|
|
328
|
+
): Promise<interfaces.data.IGatewayClientDnsRecord[]> {
|
|
329
|
+
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
330
|
+
if (!dnsManager) return [];
|
|
331
|
+
const gatewayClientId = this.resolveGatewayClientId(auth, requestedGatewayClientId);
|
|
332
|
+
const ownedRoutes = this.getOwnedRoutes(gatewayClientId);
|
|
333
|
+
const domains = await dnsManager.listDomains();
|
|
334
|
+
const records: interfaces.data.IGatewayClientDnsRecord[] = [];
|
|
335
|
+
|
|
336
|
+
for (const route of ownedRoutes) {
|
|
337
|
+
const metadata = route.metadata;
|
|
338
|
+
if (!metadata) continue;
|
|
339
|
+
const gatewayClientType = metadata.gatewayClientType || metadata.workHosterType || 'custom';
|
|
340
|
+
const routeGatewayClientId = metadata.gatewayClientId || metadata.workHosterId || '';
|
|
341
|
+
const appId = metadata.gatewayClientAppId || metadata.workAppId || '';
|
|
342
|
+
|
|
343
|
+
for (const hostname of this.getRouteHostnames(route.route)) {
|
|
344
|
+
if (auth.token?.policy?.role === 'gatewayClient' && !this.matchesHostnamePatterns(hostname, auth.token.policy.hostnamePatterns || [])) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
const domainDoc = domains.find((domain) => this.isHostnameInDomain(hostname, domain.name));
|
|
348
|
+
const domainRecords = domainDoc ? await dnsManager.listRecordsForDomain(domainDoc.id) : [];
|
|
349
|
+
const matchingRecords = domainRecords.filter((record) => record.name === hostname);
|
|
350
|
+
if (matchingRecords.length === 0) {
|
|
351
|
+
records.push({
|
|
352
|
+
id: `missing:${hostname}`,
|
|
353
|
+
domainId: domainDoc?.id || '',
|
|
354
|
+
domainName: domainDoc?.name,
|
|
355
|
+
name: hostname,
|
|
356
|
+
type: 'MISSING',
|
|
357
|
+
value: '',
|
|
358
|
+
ttl: 0,
|
|
359
|
+
source: 'local',
|
|
360
|
+
status: 'missing',
|
|
361
|
+
gatewayClientType,
|
|
362
|
+
gatewayClientId: routeGatewayClientId,
|
|
363
|
+
appId,
|
|
364
|
+
hostname,
|
|
365
|
+
routeId: route.id,
|
|
366
|
+
managePath: domainDoc ? `/domains/${domainDoc.id}/dns` : '/domains',
|
|
367
|
+
createdAt: route.createdAt || 0,
|
|
368
|
+
updatedAt: route.updatedAt || 0,
|
|
369
|
+
createdBy: '',
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
for (const recordDoc of matchingRecords) {
|
|
374
|
+
const record = dnsManager.toPublicRecord(recordDoc);
|
|
375
|
+
records.push({
|
|
376
|
+
...record,
|
|
377
|
+
domainName: domainDoc?.name,
|
|
378
|
+
status: 'active',
|
|
379
|
+
gatewayClientType,
|
|
380
|
+
gatewayClientId: routeGatewayClientId,
|
|
381
|
+
appId,
|
|
382
|
+
hostname,
|
|
383
|
+
routeId: route.id,
|
|
384
|
+
managePath: `/dns-records/${record.id}`,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return records;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
private isHostnameInDomain(hostname: string, domainName: string): boolean {
|
|
394
|
+
const normalizedHostname = hostname.trim().toLowerCase();
|
|
395
|
+
const normalizedDomainName = domainName.trim().toLowerCase();
|
|
396
|
+
return normalizedHostname === normalizedDomainName || normalizedHostname.endsWith(`.${normalizedDomainName}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private async syncGatewayClientRoute(
|
|
400
|
+
auth: TAuthContext,
|
|
401
|
+
ownership: interfaces.data.IGatewayClientOwnership,
|
|
402
|
+
route?: interfaces.data.IDcRouterRouteConfig,
|
|
403
|
+
enabled?: boolean,
|
|
404
|
+
deleteRoute?: boolean,
|
|
405
|
+
): Promise<interfaces.data.IGatewayClientRouteSyncResult> {
|
|
406
|
+
this.assertGatewayClientOwnership(auth, ownership);
|
|
407
|
+
this.assertRouteTargetsAllowed(auth, route);
|
|
408
|
+
|
|
409
|
+
const manager = this.opsServerRef.dcRouterRef.routeConfigManager;
|
|
410
|
+
if (!manager) {
|
|
411
|
+
return { success: false, message: 'Route management not initialized' };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const externalKey = this.buildGatewayClientExternalKey(ownership);
|
|
415
|
+
const existingRoute = manager.findApiRouteByExternalKey(externalKey);
|
|
416
|
+
|
|
417
|
+
if (deleteRoute) {
|
|
418
|
+
if (!existingRoute) {
|
|
419
|
+
return { success: true, action: 'unchanged' };
|
|
420
|
+
}
|
|
421
|
+
const result = await manager.deleteRoute(existingRoute.id);
|
|
422
|
+
return result.success
|
|
423
|
+
? { success: true, action: 'deleted', routeId: existingRoute.id }
|
|
424
|
+
: { success: false, message: result.message };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (!route) {
|
|
428
|
+
return { success: false, message: 'route is required unless delete=true' };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const metadata: interfaces.data.IRouteMetadata = {
|
|
432
|
+
ownerType: 'gatewayClient',
|
|
433
|
+
gatewayClientType: ownership.gatewayClientType,
|
|
434
|
+
gatewayClientId: ownership.gatewayClientId,
|
|
435
|
+
gatewayClientAppId: ownership.appId,
|
|
436
|
+
workHosterType: ownership.gatewayClientType,
|
|
437
|
+
workHosterId: ownership.gatewayClientId,
|
|
438
|
+
workAppId: ownership.appId,
|
|
439
|
+
externalKey,
|
|
440
|
+
};
|
|
441
|
+
const normalizedRoute = this.normalizeGatewayClientRoute(route, ownership, externalKey);
|
|
442
|
+
|
|
443
|
+
if (existingRoute) {
|
|
444
|
+
const result = await manager.updateRoute(existingRoute.id, {
|
|
445
|
+
route: normalizedRoute,
|
|
446
|
+
enabled: enabled ?? true,
|
|
447
|
+
metadata,
|
|
448
|
+
});
|
|
449
|
+
return result.success
|
|
450
|
+
? { success: true, action: 'updated', routeId: existingRoute.id }
|
|
451
|
+
: { success: false, message: result.message };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const routeId = await manager.createRoute(normalizedRoute, auth.userId, enabled ?? true, metadata);
|
|
455
|
+
return { success: true, action: 'created', routeId };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private buildGatewayClientExternalKey(ownership: interfaces.data.IGatewayClientOwnership): string {
|
|
459
|
+
return [
|
|
460
|
+
ownership.gatewayClientType,
|
|
461
|
+
ownership.gatewayClientId,
|
|
462
|
+
ownership.appId,
|
|
463
|
+
ownership.hostname,
|
|
464
|
+
].map((part) => part.trim()).join(':');
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private normalizeWorkAppRoute(
|
|
468
|
+
route: interfaces.data.IDcRouterRouteConfig,
|
|
469
|
+
ownership: interfaces.data.IWorkAppRouteOwnership,
|
|
470
|
+
externalKey: string,
|
|
471
|
+
): interfaces.data.IDcRouterRouteConfig {
|
|
472
|
+
const normalizedRoute = { ...route };
|
|
473
|
+
if (!normalizedRoute.name) {
|
|
474
|
+
normalizedRoute.name = `workapp-${externalKey.replace(/[^a-zA-Z0-9-]+/g, '-').slice(0, 80)}`;
|
|
475
|
+
}
|
|
476
|
+
return normalizedRoute;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
private normalizeGatewayClientRoute(
|
|
480
|
+
route: interfaces.data.IDcRouterRouteConfig,
|
|
481
|
+
ownership: interfaces.data.IGatewayClientOwnership,
|
|
482
|
+
externalKey: string,
|
|
483
|
+
): interfaces.data.IDcRouterRouteConfig {
|
|
484
|
+
const normalizedRoute = { ...route };
|
|
485
|
+
if (!normalizedRoute.name) {
|
|
486
|
+
normalizedRoute.name = `gateway-client-${externalKey.replace(/[^a-zA-Z0-9-]+/g, '-').slice(0, 80)}`;
|
|
487
|
+
}
|
|
488
|
+
return normalizedRoute;
|
|
489
|
+
}
|
|
490
|
+
}
|