@serve.zone/dcrouter 34.6.0 → 34.7.1
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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +14 -1
- package/dist_ts/classes.dcrouter.js +5 -4
- package/dist_ts/email/classes.coremail-gateway.manager.d.ts +23 -9
- package/dist_ts/email/classes.coremail-gateway.manager.js +65 -15
- package/dist_ts/email/coremail-gateway-runtime.d.ts +5 -2
- package/dist_ts/email/coremail-gateway-runtime.js +14 -10
- package/dist_ts/opsserver/classes.coremail-gateway-server.d.ts +35 -0
- package/dist_ts/opsserver/classes.coremail-gateway-server.js +91 -0
- package/dist_ts/opsserver/classes.opsserver.d.ts +5 -2
- package/dist_ts/opsserver/classes.opsserver.js +14 -3
- package/dist_ts/plugins.d.ts +3 -0
- package/dist_ts/plugins.js +8 -1
- package/dist_ts_migrations/index.js +6 -1
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +5 -1
- package/readme.md +20 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +19 -4
- package/ts/email/classes.coremail-gateway.manager.ts +117 -38
- package/ts/email/coremail-gateway-runtime.ts +21 -14
- package/ts/opsserver/classes.coremail-gateway-server.ts +104 -0
- package/ts/opsserver/classes.opsserver.ts +16 -2
- package/ts/plugins.ts +12 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/dcrouter",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "34.
|
|
4
|
+
"version": "34.7.1",
|
|
5
5
|
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"author": "Task Venture Capital GmbH",
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"devDependencies": {
|
|
18
|
+
"@api.global/typedrequest-coremail-client": "npm:@api.global/typedrequest@9.1.0",
|
|
19
|
+
"@api.global/typedsocket-coremail-client": "npm:@api.global/typedsocket@9.0.2",
|
|
18
20
|
"@git.zone/tsdeno": "^1.15.0",
|
|
19
21
|
"@git.zone/tsdocker": "^3.14.0",
|
|
20
22
|
"@git.zone/tsrepo": "^2.1.0",
|
|
@@ -29,8 +31,10 @@
|
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
33
|
"@api.global/typedrequest": "8.0.4",
|
|
34
|
+
"@api.global/typedrequest-coremail": "npm:@api.global/typedrequest@9.2.2",
|
|
32
35
|
"@api.global/typedrequest-interfaces": "7.1.0",
|
|
33
36
|
"@api.global/typedserver": "13.0.2",
|
|
37
|
+
"@api.global/typedserver-coremail": "npm:@api.global/typedserver@16.0.2",
|
|
34
38
|
"@api.global/typedsocket": "8.5.0",
|
|
35
39
|
"@apiclient.xyz/cloudflare": "^8.0.2",
|
|
36
40
|
"@aws-sdk/client-s3": "^3.1111.0",
|
package/readme.md
CHANGED
|
@@ -161,7 +161,7 @@ Bootstrap behavior:
|
|
|
161
161
|
| `baseDir` | Root directory for dcrouter runtime data. Defaults to `~/.serve.zone/dcrouter`. |
|
|
162
162
|
| `coreTrafficConfig` | Main CoreTraffic route configuration for HTTP/HTTPS/TCP/SNI traffic. `smartProxyConfig` remains accepted as a legacy alias. |
|
|
163
163
|
| `emailConfig` | UnifiedEmailServer configuration: hostname, ports, domains, and mail routes. |
|
|
164
|
-
| `emailOutboundMode` | Outbound SMTP mode
|
|
164
|
+
| `emailOutboundMode` | Outbound SMTP mode; explicit `direct` mode lets unmanaged/static senders dial from the hub. With the database enabled it seeds the stored email settings on first start only; after that the stored settings are authoritative, they default to `remoteIngress`, and the mode changes through the email settings API or Ops UI. Without the database it is read directly on every delivery and has no default: while it is unset, outbound mail defers. |
|
|
165
165
|
| `emailPortConfig` | External-to-internal email port mapping and received-email storage path. |
|
|
166
166
|
| `tls` | Legacy/static TLS and ACME contact settings used to seed certificate config. |
|
|
167
167
|
| `dnsNsDomains` | Nameserver hostnames used for generated NS records and DoH routes. A zone becomes authoritative by having its public NS records observed naming one of these. |
|
|
@@ -1172,11 +1172,27 @@ order, so the producer canonicalises the string before sending it.
|
|
|
1172
1172
|
|
|
1173
1173
|
| Setting | Owner | Canonical form |
|
|
1174
1174
|
| --- | --- | --- |
|
|
1175
|
-
| `gateway.endpointUrl` (in CoreMail's desired state) | producer | `wss://<
|
|
1175
|
+
| `gateway.endpointUrl` (in CoreMail's desired state) | producer | `wss://<gateway-host>/` — **with** the trailing slash |
|
|
1176
1176
|
| `transferOrigin` (in the peer above) | producer | `https://<coremail-host>` — origin only, **no** trailing slash |
|
|
1177
1177
|
|
|
1178
|
-
`endpointUrl`
|
|
1179
|
-
|
|
1178
|
+
Which host `endpointUrl` names depends on CoreMail's TypedSocket major, because
|
|
1179
|
+
the TypedSocket handshake refuses a peer of another wire:
|
|
1180
|
+
|
|
1181
|
+
- CoreMail up to 32 (TypedSocket 8) dials dcrouter's ops server port
|
|
1182
|
+
(`opsServerPort`, default `3000`) — the same TypedSocket surface
|
|
1183
|
+
platformclient's `MAIL_TYPED_URL` reaches.
|
|
1184
|
+
- CoreMail 33 and later (TypedSocket 9; wire 9 is shared by TypedSocket 9 and 10)
|
|
1185
|
+
dials the CoreMail gateway listener, `IDcRouterOptions.coreMailGatewayServer`.
|
|
1186
|
+
It is disabled when the option is absent. The option has `port` and a nonempty
|
|
1187
|
+
exact `hostnames` list; it always binds `127.0.0.1` and serves its socket at `/`,
|
|
1188
|
+
so the operator routes one hub hostname to that port and lists the same hostname
|
|
1189
|
+
in `hostnames` (other hosts, other paths, HTTP and browser Origins are answered
|
|
1190
|
+
`404`). In OCI mode the option goes into the `DCROUTER_CONFIG_PATH` JSON. The
|
|
1191
|
+
listener serves the three gateway calls and nothing else: no Ops, admin or
|
|
1192
|
+
workload mail methods. It authenticates with the CoreMail credential behind the
|
|
1193
|
+
same per-credential-id attempt limit as the ops surface and adds no source-address
|
|
1194
|
+
check. The ops server keeps serving CoreMail on TypedSocket 8 alongside it.
|
|
1195
|
+
|
|
1180
1196
|
`transferOrigin` is CoreMail's own HTTPS origin, from which dcrouter fetches and
|
|
1181
1197
|
to which it uploads message bytes; dcrouter returns it verbatim during
|
|
1182
1198
|
authentication and CoreMail refuses the session unless it matches its own desired
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { createApplicationMigrationRunner } from '../ts_migrations/index.js';
|
|
|
25
25
|
|
|
26
26
|
import { OpsServer } from './opsserver/index.js';
|
|
27
27
|
import type { IWorkloadMailServerOptions } from './opsserver/classes.workload-mail-server.js';
|
|
28
|
+
import type { ICoreMailGatewayServerOptions } from './opsserver/classes.coremail-gateway-server.js';
|
|
28
29
|
import { MetricsManager, OpsEventManager } from './monitoring/index.js';
|
|
29
30
|
import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
|
|
30
31
|
import { RemoteIngressHubLifecycle, RemoteIngressManager, TunnelManager, buildEdgeOnlyDerivationRoutes } from './remoteingress/index.js';
|
|
@@ -115,7 +116,13 @@ export interface IDcRouterOptions {
|
|
|
115
116
|
/** SmartBucket configuration for durable SmartMTA queue and attachment blobs. */
|
|
116
117
|
emailBlobStorage?: ISmartMtaBlobStorageConfig;
|
|
117
118
|
|
|
118
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Outbound SMTP connection mode. With the database enabled it seeds the
|
|
121
|
+
* stored email settings on first start only; after that the stored settings
|
|
122
|
+
* are authoritative, and they default to `remoteIngress`. Without the
|
|
123
|
+
* database it is read directly on every delivery and has no default: while
|
|
124
|
+
* it is unset, outbound mail defers.
|
|
125
|
+
*/
|
|
119
126
|
emailOutboundMode?: TEmailOutboundMode;
|
|
120
127
|
|
|
121
128
|
/**
|
|
@@ -230,6 +237,13 @@ export interface IDcRouterOptions {
|
|
|
230
237
|
/** Disabled unless explicitly configured; loopback-only, separately routed two-operation mail API. */
|
|
231
238
|
workloadMailServer?: IWorkloadMailServerOptions;
|
|
232
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Disabled unless explicitly configured; loopback-only listener that serves the CoreMail gateway,
|
|
242
|
+
* and nothing else, to CoreMail on TypedSocket 9 or 10, behind one hub hostname route (Linus
|
|
243
|
+
* ruling 70, L2). The Ops server keeps serving the gateway on TypedSocket 8.
|
|
244
|
+
*/
|
|
245
|
+
coreMailGatewayServer?: ICoreMailGatewayServerOptions;
|
|
246
|
+
|
|
233
247
|
/** Optional OpsServer account authentication settings. */
|
|
234
248
|
adminAuth?: {
|
|
235
249
|
/** Optional idp.global password-authentication URL override. Defaults to the SDK's hosted https://app.idp.global endpoint. Can also be set through DCROUTER_IDP_GLOBAL_URL. */
|
|
@@ -479,9 +493,10 @@ export class DcRouter {
|
|
|
479
493
|
store: new CoreMailGatewayStore(),
|
|
480
494
|
runtime: createCoreMailGatewayRuntime(this),
|
|
481
495
|
});
|
|
482
|
-
// CoreMail connects as a TypedSocket client to the ops
|
|
483
|
-
// serves this router through the OpsServer's own chain.
|
|
484
|
-
// keeps the manager independent of OpsServer construction.
|
|
496
|
+
// CoreMail on TypedSocket 8 connects as a TypedSocket client to the ops
|
|
497
|
+
// surface, which serves this router through the OpsServer's own chain.
|
|
498
|
+
// Registering here keeps the manager independent of OpsServer construction.
|
|
499
|
+
// CoreMail on wire 9 connects to the `coreMailGatewayServer` listener instead.
|
|
485
500
|
this.typedrouter.addTypedRouter(this.coreMailGatewayManager.typedrouter);
|
|
486
501
|
this.smtpAccountManager = new SmtpAccountManager(this);
|
|
487
502
|
this.acceptedEmailSpool = new AcceptedEmailSpool(this);
|
|
@@ -67,6 +67,19 @@ interface ICoreMailPeerLike {
|
|
|
67
67
|
context: { state: Record<string, unknown> };
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* A CoreMail connection on one of the sockets the gateway is served on: the Ops
|
|
72
|
+
* server's TypedSocket 8 or the CoreMail gateway listener's wire 9. A request to
|
|
73
|
+
* CoreMail goes out on the socket the connection belongs to.
|
|
74
|
+
*/
|
|
75
|
+
export interface ICoreMailGatewayConnection {
|
|
76
|
+
peer: ICoreMailPeerLike;
|
|
77
|
+
fire<T extends plugins.typedrequestInterfaces.ITypedRequest>(
|
|
78
|
+
methodArg: T['method'],
|
|
79
|
+
requestArg: T['request'],
|
|
80
|
+
): Promise<T['response']>;
|
|
81
|
+
}
|
|
82
|
+
|
|
70
83
|
interface ICoreMailQueueItemLike {
|
|
71
84
|
id: string;
|
|
72
85
|
status: 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred';
|
|
@@ -87,11 +100,8 @@ export interface ICoreMailGatewayRuntime {
|
|
|
87
100
|
idempotencyKeyArg: string,
|
|
88
101
|
): Promise<string>;
|
|
89
102
|
getQueueItem(queueItemIdArg: string): ICoreMailQueueItemLike | undefined;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
methodArg: T['method'],
|
|
93
|
-
connectionArg: unknown,
|
|
94
|
-
): { fire(requestArg: T['request']): Promise<T['response']> };
|
|
103
|
+
/** The connection with this routing tag on any socket the gateway is served on. */
|
|
104
|
+
findConnection(tagArg: string): Promise<ICoreMailGatewayConnection | undefined>;
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
export interface ICoreMailGatewayManagerOptions {
|
|
@@ -121,14 +131,55 @@ export class CoreMailInboundRefusedError extends Error {
|
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
133
|
|
|
134
|
+
/**
|
|
135
|
+
* The contract error a gateway request is refused with. It is definitive
|
|
136
|
+
* unless it names when to retry: CoreMail takes a non-retryable
|
|
137
|
+
* `AUTHENTICATION_FAILED` as the verdict on its credential and stops
|
|
138
|
+
* connecting with it.
|
|
139
|
+
*/
|
|
124
140
|
const contractError = (
|
|
125
141
|
codeArg: TErrorCode,
|
|
126
|
-
|
|
142
|
+
retryAfterMsArg?: number,
|
|
127
143
|
): plugins.typedrequest.TypedResponseError =>
|
|
128
|
-
new plugins.typedrequest.TypedResponseError(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
144
|
+
new plugins.typedrequest.TypedResponseError(
|
|
145
|
+
'CoreMail gateway request rejected.',
|
|
146
|
+
(retryAfterMsArg === undefined
|
|
147
|
+
? { code: codeArg, retryable: false }
|
|
148
|
+
: { code: codeArg, retryable: true, retryAfterMs: retryAfterMsArg }
|
|
149
|
+
) satisfies plugins.servezoneInterfaces.data.ICoreMailErrorData,
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Whether a replayed envelope is the one the stored handoff was prepared with.
|
|
154
|
+
* Compares values, never encodings: the stored envelope is whatever the
|
|
155
|
+
* document store returns, and its key order is not part of the data. The
|
|
156
|
+
* recipient order is, as it is in the submission digest.
|
|
157
|
+
*/
|
|
158
|
+
const isSameCoreMailEnvelope = (
|
|
159
|
+
storedArg: plugins.servezoneInterfaces.data.ICoreMailEnvelope,
|
|
160
|
+
replayedArg: plugins.servezoneInterfaces.data.ICoreMailEnvelope,
|
|
161
|
+
): boolean =>
|
|
162
|
+
storedArg.mailFrom === replayedArg.mailFrom
|
|
163
|
+
&& storedArg.rcptTo.length === replayedArg.rcptTo.length
|
|
164
|
+
&& storedArg.rcptTo.every((recipientArg, indexArg) => recipientArg === replayedArg.rcptTo[indexArg]);
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Answers a wire-9 request with the handler's own contract refusal: TypedRequest 9
|
|
168
|
+
* answers an error of another class, TypedRequest 8's `TypedResponseError`
|
|
169
|
+
* included, as an internal error without its `errorData`.
|
|
170
|
+
*/
|
|
171
|
+
const onCoreMailGatewayWire = <TRequest, TResponse>(
|
|
172
|
+
handlerArg: (requestArg: TRequest, toolsArg: unknown) => Promise<TResponse>,
|
|
173
|
+
) => async (requestArg: TRequest, toolsArg?: unknown): Promise<TResponse> => {
|
|
174
|
+
try {
|
|
175
|
+
return await handlerArg(requestArg, toolsArg);
|
|
176
|
+
} catch (error: unknown) {
|
|
177
|
+
if (error instanceof plugins.typedrequest.TypedResponseError) {
|
|
178
|
+
throw new plugins.typedrequestCoreMail.TypedResponseError(error.errorText, error.errorData);
|
|
179
|
+
}
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
132
183
|
|
|
133
184
|
const toTimestamp = (valueArg: Date | number | undefined): number | undefined => {
|
|
134
185
|
if (valueArg === undefined) return undefined;
|
|
@@ -146,6 +197,7 @@ const toTimestamp = (valueArg: Date | number | undefined): number | undefined =>
|
|
|
146
197
|
* `…CompleteInboundHandoff` and `…CompleteOutboundHandoff` back on the peer.
|
|
147
198
|
*/
|
|
148
199
|
export class CoreMailGatewayManager {
|
|
200
|
+
/** The gateway's handlers on TypedSocket 8, which the Ops server serves. */
|
|
149
201
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
150
202
|
|
|
151
203
|
private readonly store: ICoreMailGatewayStore;
|
|
@@ -184,7 +236,7 @@ export class CoreMailGatewayManager {
|
|
|
184
236
|
this.verifyCredential = optionsArg.verifyCredential
|
|
185
237
|
?? (async (hashArg, secretArg) => await plugins.argon2.verify(hashArg, secretArg));
|
|
186
238
|
this.enqueueRetryDelaysMs = optionsArg.enqueueRetryDelaysMs ?? [1_000, 5_000, 15_000];
|
|
187
|
-
this.
|
|
239
|
+
this.registerGatewayHandlers(this.typedrouter);
|
|
188
240
|
}
|
|
189
241
|
|
|
190
242
|
// -------------------------------------------------------- peer provisioning
|
|
@@ -274,31 +326,52 @@ export class CoreMailGatewayManager {
|
|
|
274
326
|
|
|
275
327
|
// ---------------------------------------------------------------- handlers
|
|
276
328
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Registers the gateway's three handlers on the router it is handed (Linus
|
|
331
|
+
* rulings 39, 3f, and 70, L2): this manager's `typedrouter` (TypedSocket 8,
|
|
332
|
+
* served by the Ops server) and the CoreMail gateway listener's router (wire 9,
|
|
333
|
+
* `CoreMailGatewayServer`). Both run the same handler bodies, and the session
|
|
334
|
+
* an authentication sets lives on the peer of the socket it arrived on.
|
|
335
|
+
*/
|
|
336
|
+
public registerGatewayHandlers(
|
|
337
|
+
routerArg: plugins.typedrequest.TypedRouter | plugins.typedrequestCoreMail.TypedRouter,
|
|
338
|
+
): void {
|
|
339
|
+
const authenticate = async (dataArg: TReq_Authenticate['request'], toolsArg: unknown) =>
|
|
340
|
+
await this.authenticate(this.requirePeer(toolsArg), dataArg);
|
|
341
|
+
const prepareOutboundHandoff = async (
|
|
342
|
+
dataArg: TReq_PrepareOutboundHandoff['request'],
|
|
343
|
+
toolsArg: unknown,
|
|
344
|
+
) => await this.prepareOutboundHandoff(this.requireSession(this.requirePeer(toolsArg)), dataArg);
|
|
345
|
+
const getOutboundStatus = async (dataArg: TReq_GetOutboundStatus['request'], toolsArg: unknown) =>
|
|
346
|
+
await this.getOutboundStatus(this.requireSession(this.requirePeer(toolsArg)), dataArg);
|
|
347
|
+
|
|
348
|
+
if (routerArg instanceof plugins.typedrequestCoreMail.TypedRouter) {
|
|
349
|
+
routerArg.addTypedHandler(new plugins.typedrequestCoreMail.TypedHandler<TReq_Authenticate>(
|
|
280
350
|
'coreMailGatewayAuthenticate',
|
|
281
|
-
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
this.typedrouter.addTypedHandler(
|
|
285
|
-
new plugins.typedrequest.TypedHandler<TReq_PrepareOutboundHandoff>(
|
|
351
|
+
onCoreMailGatewayWire(authenticate),
|
|
352
|
+
));
|
|
353
|
+
routerArg.addTypedHandler(new plugins.typedrequestCoreMail.TypedHandler<TReq_PrepareOutboundHandoff>(
|
|
286
354
|
'coreMailGatewayPrepareOutboundHandoff',
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
),
|
|
291
|
-
),
|
|
292
|
-
);
|
|
293
|
-
this.typedrouter.addTypedHandler(
|
|
294
|
-
new plugins.typedrequest.TypedHandler<TReq_GetOutboundStatus>(
|
|
355
|
+
onCoreMailGatewayWire(prepareOutboundHandoff),
|
|
356
|
+
));
|
|
357
|
+
routerArg.addTypedHandler(new plugins.typedrequestCoreMail.TypedHandler<TReq_GetOutboundStatus>(
|
|
295
358
|
'coreMailGatewayGetOutboundStatus',
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
359
|
+
onCoreMailGatewayWire(getOutboundStatus),
|
|
360
|
+
));
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler<TReq_Authenticate>(
|
|
364
|
+
'coreMailGatewayAuthenticate',
|
|
365
|
+
authenticate,
|
|
366
|
+
));
|
|
367
|
+
routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler<TReq_PrepareOutboundHandoff>(
|
|
368
|
+
'coreMailGatewayPrepareOutboundHandoff',
|
|
369
|
+
prepareOutboundHandoff,
|
|
370
|
+
));
|
|
371
|
+
routerArg.addTypedHandler(new plugins.typedrequest.TypedHandler<TReq_GetOutboundStatus>(
|
|
372
|
+
'coreMailGatewayGetOutboundStatus',
|
|
373
|
+
getOutboundStatus,
|
|
374
|
+
));
|
|
302
375
|
}
|
|
303
376
|
|
|
304
377
|
private requirePeer(toolsArg: unknown): ICoreMailPeerLike {
|
|
@@ -320,8 +393,9 @@ export class CoreMailGatewayManager {
|
|
|
320
393
|
|
|
321
394
|
/**
|
|
322
395
|
* Every gateway method other than the handshake requires an authenticated
|
|
323
|
-
* CoreMail session on this exact connection. The
|
|
324
|
-
* shared with other client types
|
|
396
|
+
* CoreMail session on this exact connection. The Ops TypedSocket surface is
|
|
397
|
+
* shared with other client types and the gateway listener takes any peer that
|
|
398
|
+
* reaches it, so presence on a socket proves nothing.
|
|
325
399
|
*/
|
|
326
400
|
private requireSession(peerArg: ICoreMailPeerLike): ICoreMailGatewaySession {
|
|
327
401
|
const session = peerArg.data.get(coreMailGatewaySessionKey) as
|
|
@@ -353,7 +427,12 @@ export class CoreMailGatewayManager {
|
|
|
353
427
|
bucket.refilledAt += intervals * 1_000;
|
|
354
428
|
}
|
|
355
429
|
if (bucket.tokens < 1) {
|
|
356
|
-
|
|
430
|
+
// A spent budget says nothing about the credential, so the refusal is
|
|
431
|
+
// retryable once the next token refills.
|
|
432
|
+
throw contractError(
|
|
433
|
+
'AUTHENTICATION_FAILED',
|
|
434
|
+
Math.max(1, Math.ceil(bucket.refilledAt + 1_000 - now)),
|
|
435
|
+
);
|
|
357
436
|
}
|
|
358
437
|
bucket.tokens -= 1;
|
|
359
438
|
}
|
|
@@ -515,7 +594,7 @@ export class CoreMailGatewayManager {
|
|
|
515
594
|
|| existing.submissionDigest !== submissionDigest
|
|
516
595
|
|| existing.rawMimeSha256 !== message.rawMime.sha256
|
|
517
596
|
|| existing.rawMimeLengthBytes !== message.rawMime.lengthBytes
|
|
518
|
-
||
|
|
597
|
+
|| !isSameCoreMailEnvelope(existing.envelope, message.envelope)
|
|
519
598
|
) {
|
|
520
599
|
throw contractError('IDEMPOTENCY_CONFLICT');
|
|
521
600
|
}
|
|
@@ -929,7 +1008,7 @@ export class CoreMailGatewayManager {
|
|
|
929
1008
|
if (!connection || !session || session.coreMailServiceId !== coreMailServiceIdArg) {
|
|
930
1009
|
throw new Error('No authenticated CoreMail gateway peer is connected');
|
|
931
1010
|
}
|
|
932
|
-
return await
|
|
1011
|
+
return await connection.fire<T>(methodArg, requestArg);
|
|
933
1012
|
}
|
|
934
1013
|
|
|
935
1014
|
private async isPeerConnected(coreMailServiceIdArg: string): Promise<boolean> {
|
|
@@ -1,13 +1,26 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
2
|
import type { DcRouter } from '../classes.dcrouter.js';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ICoreMailGatewayConnection,
|
|
5
|
+
ICoreMailGatewayRuntime,
|
|
6
|
+
} from './classes.coremail-gateway.manager.js';
|
|
7
|
+
|
|
8
|
+
/** A CoreMail connection on a TypedSocket 8 server, with requests bound to that socket. */
|
|
9
|
+
export const toCoreMailGatewayConnection = (
|
|
10
|
+
typedsocketArg: plugins.typedsocket.TypedSocket,
|
|
11
|
+
connectionArg: plugins.typedsocket.ISmartServeConnectionWrapper,
|
|
12
|
+
): ICoreMailGatewayConnection => ({
|
|
13
|
+
peer: connectionArg.peer,
|
|
14
|
+
fire: async (methodArg, requestArg) =>
|
|
15
|
+
await typedsocketArg.createTypedRequest(methodArg, connectionArg).fire(requestArg),
|
|
16
|
+
});
|
|
4
17
|
|
|
5
18
|
/**
|
|
6
19
|
* Binds the CoreMail gateway manager to the live dcrouter runtime.
|
|
7
20
|
*
|
|
8
21
|
* Everything the manager needs from dcrouter passes through this adapter, so
|
|
9
22
|
* the manager stays testable against a stub and dcrouter keeps ownership of
|
|
10
|
-
* the smartmta server and the
|
|
23
|
+
* the smartmta server and the sockets the gateway is served on.
|
|
11
24
|
*/
|
|
12
25
|
export const createCoreMailGatewayRuntime = (
|
|
13
26
|
dcRouterArg: DcRouter,
|
|
@@ -28,20 +41,14 @@ export const createCoreMailGatewayRuntime = (
|
|
|
28
41
|
|
|
29
42
|
getQueueItem: (queueItemIdArg) => dcRouterArg.emailServer?.getQueueItem(queueItemIdArg),
|
|
30
43
|
|
|
44
|
+
// The CoreMail gateway listener first: a CoreMail on TypedSocket 9 or 10 can
|
|
45
|
+
// reach only that one. A CoreMail on TypedSocket 8 reaches only the Ops server.
|
|
31
46
|
findConnection: async (tagArg) => {
|
|
47
|
+
const listenerConnection = await dcRouterArg.opsServer?.coreMailGatewayServer?.findConnection(tagArg);
|
|
48
|
+
if (listenerConnection) return listenerConnection;
|
|
32
49
|
const typedsocket = dcRouterArg.opsServer?.server?.typedserver?.typedsocket;
|
|
33
50
|
if (!typedsocket) return undefined;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
createRequest: (methodArg, connectionArg) => {
|
|
38
|
-
const typedsocket = dcRouterArg.opsServer?.server?.typedserver?.typedsocket;
|
|
39
|
-
if (!typedsocket) {
|
|
40
|
-
throw new Error('OpsServer TypedSocket is not available for CoreMail gateway requests');
|
|
41
|
-
}
|
|
42
|
-
return typedsocket.createTypedRequest(
|
|
43
|
-
methodArg as never,
|
|
44
|
-
connectionArg as plugins.typedsocket.ISmartServeConnectionWrapper,
|
|
45
|
-
) as never;
|
|
51
|
+
const connection = await typedsocket.findTargetConnectionByTag(tagArg);
|
|
52
|
+
return connection ? toCoreMailGatewayConnection(typedsocket, connection) : undefined;
|
|
46
53
|
},
|
|
47
54
|
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
CoreMailGatewayManager,
|
|
4
|
+
ICoreMailGatewayConnection,
|
|
5
|
+
} from '../email/classes.coremail-gateway.manager.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The one path the listener serves. The URL CoreMail dials, Cloudly's
|
|
9
|
+
* `coreMailGatewayEndpointUrl`, is a bare wss origin, so the socket sits at the
|
|
10
|
+
* root of the hub hostname that routes to this listener.
|
|
11
|
+
*/
|
|
12
|
+
export const COREMAIL_GATEWAY_SOCKET_PATH = '/';
|
|
13
|
+
|
|
14
|
+
/** Opt-in loopback listener (Linus ruling 70, L2). Its hub hostname route is the operator's. */
|
|
15
|
+
export interface ICoreMailGatewayServerOptions {
|
|
16
|
+
/** Port on 127.0.0.1; 0 asks the OS for one. */
|
|
17
|
+
port: number;
|
|
18
|
+
/** Hostnames of the hub route that forwards to this port; any other Host is answered 404. */
|
|
19
|
+
hostnames: readonly string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type TCoreMailGatewayTypedServer = plugins.typedserverCoreMail.TypedServer;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The CoreMail gateway on wire 9 (TypedSocket 9 and 10), separate from the Ops
|
|
26
|
+
* server, which keeps serving it on TypedSocket 8. It serves the gateway's
|
|
27
|
+
* three handlers and nothing else: no Ops router, admin or workload mail, no
|
|
28
|
+
* HTTP RPC, no client tags and no built-in methods. Authentication is the
|
|
29
|
+
* gateway's own, the CoreMail credential behind the per-credential-id attempt
|
|
30
|
+
* limiter; the listener adds no source check.
|
|
31
|
+
*/
|
|
32
|
+
export class CoreMailGatewayServer {
|
|
33
|
+
private server?: TCoreMailGatewayTypedServer;
|
|
34
|
+
/** Peers are looked up only between a completed start and the beginning of stop. */
|
|
35
|
+
private serving = false;
|
|
36
|
+
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly options: ICoreMailGatewayServerOptions,
|
|
39
|
+
private readonly manager: Pick<CoreMailGatewayManager, 'registerGatewayHandlers'>,
|
|
40
|
+
) {}
|
|
41
|
+
|
|
42
|
+
public get listeningPort(): number | null {
|
|
43
|
+
return this.server?.listeningPort ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async start(): Promise<void> {
|
|
47
|
+
if (this.server) throw new Error('CoreMail gateway server already started');
|
|
48
|
+
if (!Number.isInteger(this.options.port) || this.options.port < 0 || this.options.port > 65535) {
|
|
49
|
+
throw new Error('CoreMail gateway port must be an integer from 0 to 65535');
|
|
50
|
+
}
|
|
51
|
+
const router = new plugins.typedrequestCoreMail.TypedRouter();
|
|
52
|
+
this.manager.registerGatewayHandlers(router);
|
|
53
|
+
const server = new plugins.typedserverCoreMail.TypedServer({
|
|
54
|
+
port: this.options.port,
|
|
55
|
+
listenHostname: '127.0.0.1',
|
|
56
|
+
cors: false,
|
|
57
|
+
surfaces: [{
|
|
58
|
+
name: 'coremail-gateway',
|
|
59
|
+
match: { hostnames: this.options.hostnames, pathPrefixes: [COREMAIL_GATEWAY_SOCKET_PATH] },
|
|
60
|
+
websocketTypedRouter: router,
|
|
61
|
+
typedRequestPath: false,
|
|
62
|
+
includeBuiltinTypedHandlers: false,
|
|
63
|
+
healthzEndpoint: false,
|
|
64
|
+
cors: false,
|
|
65
|
+
requestAdmission: () => new Response('Not Found', { status: 404 }),
|
|
66
|
+
websocketAdmission: (contextArg) => (
|
|
67
|
+
contextArg.url.pathname === COREMAIL_GATEWAY_SOCKET_PATH
|
|
68
|
+
&& !contextArg.url.search
|
|
69
|
+
&& !contextArg.request.headers.has('origin')
|
|
70
|
+
) ? true : new Response('Not Found', { status: 404 }),
|
|
71
|
+
}],
|
|
72
|
+
});
|
|
73
|
+
this.server = server;
|
|
74
|
+
try {
|
|
75
|
+
await server.start();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
try { await this.stop(); } catch (cleanupError) {
|
|
78
|
+
throw new AggregateError([error, cleanupError], 'CoreMail gateway startup and cleanup failed');
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
this.serving = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The CoreMail connection with this routing tag on this listener, requests bound to its socket. */
|
|
86
|
+
public async findConnection(tagArg: string): Promise<ICoreMailGatewayConnection | undefined> {
|
|
87
|
+
const server = this.server;
|
|
88
|
+
if (!server || !this.serving) return undefined;
|
|
89
|
+
const connection = await server.typedsocket.findTargetConnectionByTag(tagArg);
|
|
90
|
+
if (!connection) return undefined;
|
|
91
|
+
return {
|
|
92
|
+
peer: connection.peer,
|
|
93
|
+
fire: async (methodArg, requestArg) =>
|
|
94
|
+
await server.typedsocket.createTypedRequest(methodArg, connection).fire(requestArg),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public async stop(): Promise<void> {
|
|
99
|
+
this.serving = false;
|
|
100
|
+
const server = this.server;
|
|
101
|
+
await server?.stop();
|
|
102
|
+
if (this.server === server) this.server = undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -8,6 +8,7 @@ import * as interfaces from '../../ts_interfaces/index.js';
|
|
|
8
8
|
import { McpManager } from './classes.mcpmanager.js';
|
|
9
9
|
import { createLogStreamAuthorizationAdapter } from './helpers/logstream-auth.js';
|
|
10
10
|
import { WorkloadMailServer } from './classes.workload-mail-server.js';
|
|
11
|
+
import { CoreMailGatewayServer } from './classes.coremail-gateway-server.js';
|
|
11
12
|
import type { IListeningSocket } from '../config/helpers.listeners.js';
|
|
12
13
|
|
|
13
14
|
export class OpsServer {
|
|
@@ -58,6 +59,8 @@ export class OpsServer {
|
|
|
58
59
|
// Detach the edges start() adds to the dcRouter's main typedrouter, which outlives this server.
|
|
59
60
|
private dcRouterEdgeDisposers: Array<() => void> = [];
|
|
60
61
|
private workloadMailServer?: WorkloadMailServer;
|
|
62
|
+
/** The CoreMail gateway listener (wire 9) while it runs; absent unless configured. */
|
|
63
|
+
public coreMailGatewayServer?: CoreMailGatewayServer;
|
|
61
64
|
/** The port the Ops server listens on from its completed start until its stop begins. */
|
|
62
65
|
private listeningPort?: number;
|
|
63
66
|
|
|
@@ -67,8 +70,8 @@ export class OpsServer {
|
|
|
67
70
|
|
|
68
71
|
/**
|
|
69
72
|
* The Ops server's socket once its start completed: typedserver binds every address (`0.0.0.0`)
|
|
70
|
-
* and refuses `start()` on an address another socket holds. The workload mail
|
|
71
|
-
* 127.0.0.1 only and
|
|
73
|
+
* and refuses `start()` on an address another socket holds. The workload mail and CoreMail
|
|
74
|
+
* gateway listeners bind 127.0.0.1 only and are never public, so they are not listed.
|
|
72
75
|
*/
|
|
73
76
|
public getListeningSockets(): IListeningSocket[] {
|
|
74
77
|
return this.listeningPort === undefined
|
|
@@ -107,6 +110,13 @@ export class OpsServer {
|
|
|
107
110
|
);
|
|
108
111
|
await this.workloadMailServer.start();
|
|
109
112
|
}
|
|
113
|
+
if (this.dcRouterRef.options.coreMailGatewayServer) {
|
|
114
|
+
this.coreMailGatewayServer = new CoreMailGatewayServer(
|
|
115
|
+
this.dcRouterRef.options.coreMailGatewayServer,
|
|
116
|
+
this.dcRouterRef.coreMailGatewayManager,
|
|
117
|
+
);
|
|
118
|
+
await this.coreMailGatewayServer.start();
|
|
119
|
+
}
|
|
110
120
|
} catch (startError) {
|
|
111
121
|
// Nobody stops a server whose start failed (ServiceManager rolls back only the services that
|
|
112
122
|
// started), so undo this start here and keep its error first.
|
|
@@ -241,6 +251,10 @@ export class OpsServer {
|
|
|
241
251
|
await this.workloadMailServer?.stop();
|
|
242
252
|
this.workloadMailServer = undefined;
|
|
243
253
|
}),
|
|
254
|
+
attempt(async () => {
|
|
255
|
+
await this.coreMailGatewayServer?.stop();
|
|
256
|
+
this.coreMailGatewayServer = undefined;
|
|
257
|
+
}),
|
|
244
258
|
attempt(() => this.server?.stop()),
|
|
245
259
|
attempt(() => this.logsHandler?.cleanup()),
|
|
246
260
|
]);
|
package/ts/plugins.ts
CHANGED
|
@@ -55,6 +55,18 @@ export {
|
|
|
55
55
|
typedsocket,
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
// The CoreMail gateway listener's wire (Linus ruling 70, L1 and L2): TypedServer 16 with its
|
|
59
|
+
// TypedSocket 10 (wire major 9) and the TypedRequest 9 copy that TypedServer resolves, under aliases
|
|
60
|
+
// only `CoreMailGatewayServer` and the gateway handlers it serves use. Everything else stays on the
|
|
61
|
+
// TypedSocket 8 packages above.
|
|
62
|
+
import * as typedrequestCoreMail from '@api.global/typedrequest-coremail';
|
|
63
|
+
import * as typedserverCoreMail from '@api.global/typedserver-coremail';
|
|
64
|
+
|
|
65
|
+
export {
|
|
66
|
+
typedrequestCoreMail,
|
|
67
|
+
typedserverCoreMail,
|
|
68
|
+
}
|
|
69
|
+
|
|
58
70
|
// @idp.global scope
|
|
59
71
|
import * as idpSdkServer from '@idp.global/sdk/server';
|
|
60
72
|
|