@serve.zone/dcrouter 13.25.0 → 13.26.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 +2 -2
- package/dist_ts/config/classes.api-token-manager.js +31 -3
- 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 +32 -1
- 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
|
@@ -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
|
+
}
|
package/ts/readme.md
CHANGED
|
@@ -79,7 +79,7 @@ await router.start();
|
|
|
79
79
|
|
|
80
80
|
## License and Legal Information
|
|
81
81
|
|
|
82
|
-
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [
|
|
82
|
+
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](../license) file.
|
|
83
83
|
|
|
84
84
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
|
85
85
|
|
|
@@ -91,7 +91,7 @@ Use of these trademarks must comply with Task Venture Capital GmbH's Trademark G
|
|
|
91
91
|
|
|
92
92
|
### Company Information
|
|
93
93
|
|
|
94
|
-
Task Venture Capital GmbH
|
|
94
|
+
Task Venture Capital GmbH
|
|
95
95
|
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
96
96
|
|
|
97
97
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
|
@@ -10,6 +10,7 @@ import { ConfigManager } from './classes.config.js';
|
|
|
10
10
|
import { LogManager } from './classes.logs.js';
|
|
11
11
|
import { EmailManager } from './classes.email.js';
|
|
12
12
|
import { RadiusManager } from './classes.radius.js';
|
|
13
|
+
import { WorkHosterManager } from './classes.workhoster.js';
|
|
13
14
|
|
|
14
15
|
export interface IDcRouterApiClientOptions {
|
|
15
16
|
baseUrl: string;
|
|
@@ -31,6 +32,7 @@ export class DcRouterApiClient {
|
|
|
31
32
|
public logs: LogManager;
|
|
32
33
|
public emails: EmailManager;
|
|
33
34
|
public radius: RadiusManager;
|
|
35
|
+
public workHosters: WorkHosterManager;
|
|
34
36
|
|
|
35
37
|
constructor(options: IDcRouterApiClientOptions) {
|
|
36
38
|
this.baseUrl = options.baseUrl.replace(/\/+$/, '');
|
|
@@ -45,6 +47,7 @@ export class DcRouterApiClient {
|
|
|
45
47
|
this.logs = new LogManager(this);
|
|
46
48
|
this.emails = new EmailManager(this);
|
|
47
49
|
this.radius = new RadiusManager(this);
|
|
50
|
+
this.workHosters = new WorkHosterManager(this);
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
// =====================
|