@serve.zone/coremail 1.0.2 → 1.2.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/changelog.md +48 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.auth.d.ts +26 -7
- package/dist_ts/classes.auth.js +153 -44
- package/dist_ts/classes.config.js +6 -3
- package/dist_ts/classes.desiredstate.js +3 -3
- package/dist_ts/classes.gateway.d.ts +1 -0
- package/dist_ts/classes.gateway.js +26 -11
- package/dist_ts/classes.maintenance.js +4 -3
- package/dist_ts/classes.server.d.ts +10 -0
- package/dist_ts/classes.server.js +19 -2
- package/dist_ts/classes.transfer.js +13 -6
- package/dist_ts/coremail.log.d.ts +6 -1
- package/dist_ts/coremail.log.js +21 -3
- package/dist_ts/index.js +5 -2
- package/dist_ts/interfaces.d.ts +2 -1
- package/package.json +11 -11
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.auth.ts +215 -59
- package/ts/classes.config.ts +7 -2
- package/ts/classes.desiredstate.ts +2 -2
- package/ts/classes.gateway.ts +26 -10
- package/ts/classes.maintenance.ts +3 -2
- package/ts/classes.server.ts +19 -5
- package/ts/classes.transfer.ts +12 -5
- package/ts/coremail.log.ts +41 -3
- package/ts/index.ts +4 -1
- package/ts/interfaces.ts +2 -1
package/dist_ts/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CoreMail } from './classes.coremail.js';
|
|
2
|
+
import { logCoreMailFailure } from './coremail.log.js';
|
|
2
3
|
export { CoreMail } from './classes.coremail.js';
|
|
3
4
|
export { readCoreMailConfig } from './classes.config.js';
|
|
4
5
|
export const runCli = async () => {
|
|
@@ -14,7 +15,9 @@ export const runCli = async () => {
|
|
|
14
15
|
await coreMail.stop();
|
|
15
16
|
process.exitCode = 0;
|
|
16
17
|
}
|
|
17
|
-
catch {
|
|
18
|
+
catch (error) {
|
|
19
|
+
await logCoreMailFailure('lifecycle', 'SHUTDOWN_FAILED', error)
|
|
20
|
+
.catch(() => undefined);
|
|
18
21
|
process.exitCode = 1;
|
|
19
22
|
}
|
|
20
23
|
};
|
|
@@ -36,4 +39,4 @@ export const runCli = async () => {
|
|
|
36
39
|
throw error;
|
|
37
40
|
}
|
|
38
41
|
};
|
|
39
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
42
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi90cy9pbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDakQsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFFdkQsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLHVCQUF1QixDQUFDO0FBQ2pELE9BQU8sRUFBRSxrQkFBa0IsRUFBRSxNQUFNLHFCQUFxQixDQUFDO0FBR3pELE1BQU0sQ0FBQyxNQUFNLE1BQU0sR0FBRyxLQUFLLElBQW1CLEVBQUU7SUFDOUMsTUFBTSxRQUFRLEdBQUcsSUFBSSxRQUFRLEVBQUUsQ0FBQztJQUNoQyxJQUFJLFFBQVEsR0FBRyxLQUFLLENBQUM7SUFDckIsTUFBTSxJQUFJLEdBQUcsS0FBSyxJQUFtQixFQUFFO1FBQ3JDLElBQUksUUFBUTtZQUFFLE9BQU87UUFDckIsUUFBUSxHQUFHLElBQUksQ0FBQztRQUNoQixPQUFPLENBQUMsR0FBRyxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQztRQUNoQyxPQUFPLENBQUMsR0FBRyxDQUFDLFNBQVMsRUFBRSxTQUFTLENBQUMsQ0FBQztRQUNsQyxJQUFJLENBQUM7WUFDSCxNQUFNLFFBQVEsQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUN0QixPQUFPLENBQUMsUUFBUSxHQUFHLENBQUMsQ0FBQztRQUN2QixDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLE1BQU0sa0JBQWtCLENBQUMsV0FBVyxFQUFFLGlCQUFpQixFQUFFLEtBQUssQ0FBQztpQkFDNUQsS0FBSyxDQUFDLEdBQUcsRUFBRSxDQUFDLFNBQVMsQ0FBQyxDQUFDO1lBQzFCLE9BQU8sQ0FBQyxRQUFRLEdBQUcsQ0FBQyxDQUFDO1FBQ3ZCLENBQUM7SUFDSCxDQUFDLENBQUM7SUFDRixNQUFNLFFBQVEsR0FBRyxHQUFHLEVBQUU7UUFDcEIsS0FBSyxJQUFJLEVBQUUsQ0FBQztJQUNkLENBQUMsQ0FBQztJQUNGLE1BQU0sU0FBUyxHQUFHLEdBQUcsRUFBRTtRQUNyQixLQUFLLElBQUksRUFBRSxDQUFDO0lBQ2QsQ0FBQyxDQUFDO0lBQ0YsT0FBTyxDQUFDLEVBQUUsQ0FBQyxRQUFRLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDL0IsT0FBTyxDQUFDLEVBQUUsQ0FBQyxTQUFTLEVBQUUsU0FBUyxDQUFDLENBQUM7SUFDakMsSUFBSSxDQUFDO1FBQ0gsTUFBTSxRQUFRLENBQUMsS0FBSyxFQUFFLENBQUM7SUFDekIsQ0FBQztJQUFDLE9BQU8sS0FBSyxFQUFFLENBQUM7UUFDZixPQUFPLENBQUMsR0FBRyxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsQ0FBQztRQUNoQyxPQUFPLENBQUMsR0FBRyxDQUFDLFNBQVMsRUFBRSxTQUFTLENBQUMsQ0FBQztRQUNsQyxNQUFNLFFBQVEsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxLQUFLLENBQUMsR0FBRyxFQUFFLENBQUMsU0FBUyxDQUFDLENBQUM7UUFDN0MsTUFBTSxLQUFLLENBQUM7SUFDZCxDQUFDO0FBQ0gsQ0FBQyxDQUFDIn0=
|
package/dist_ts/interfaces.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/coremail",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Authenticated mail persistence and delivery orchestration for serve.zone workloads.",
|
|
5
5
|
"main": "dist_ts/index.js",
|
|
6
6
|
"typings": "dist_ts/index.d.ts",
|
|
@@ -8,24 +8,24 @@
|
|
|
8
8
|
"author": "Task Venture Capital GmbH",
|
|
9
9
|
"license": "MIT",
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"@git.zone/tsbuild": "^4.4.
|
|
12
|
-
"@git.zone/tsdocker": "^3.
|
|
11
|
+
"@git.zone/tsbuild": "^4.4.3",
|
|
12
|
+
"@git.zone/tsdocker": "^3.5.8",
|
|
13
13
|
"@git.zone/tsrun": "^2.0.6",
|
|
14
|
-
"@git.zone/tstest": "
|
|
14
|
+
"@git.zone/tstest": "6.0.0",
|
|
15
15
|
"@git.zone/tswatch": "^3.3.5",
|
|
16
|
-
"@push.rocks/smartmongo": "
|
|
17
|
-
"@types/node": "
|
|
16
|
+
"@push.rocks/smartmongo": "9.0.0",
|
|
17
|
+
"@types/node": "26.5.0"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@api.global/typedrequest": "^
|
|
21
|
-
"@api.global/typedrequest-interfaces": "^
|
|
22
|
-
"@api.global/typedserver": "^
|
|
23
|
-
"@api.global/typedsocket": "^
|
|
20
|
+
"@api.global/typedrequest": "^8.0.4",
|
|
21
|
+
"@api.global/typedrequest-interfaces": "^7.1.0",
|
|
22
|
+
"@api.global/typedserver": "^11.4.1",
|
|
23
|
+
"@api.global/typedsocket": "^8.2.1",
|
|
24
24
|
"@push.rocks/projectinfo": "^5.1.0",
|
|
25
25
|
"@push.rocks/smartbucket": "^4.11.0",
|
|
26
26
|
"@push.rocks/smartdata": "^10.4.0",
|
|
27
27
|
"@push.rocks/smartlog": "^3.2.2",
|
|
28
|
-
"@serve.zone/interfaces": "
|
|
28
|
+
"@serve.zone/interfaces": "30.12.0",
|
|
29
29
|
"argon2": "^0.45.1"
|
|
30
30
|
},
|
|
31
31
|
"files": [
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/classes.auth.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
requireCoreMailIdentifier,
|
|
11
11
|
requireCoreMailSafeInteger,
|
|
12
12
|
} from './coremail.selectors.js';
|
|
13
|
+
import { logCoreMailFailure } from './coremail.log.js';
|
|
13
14
|
|
|
14
15
|
type TCoreMailDesiredState = plugins.serveZoneInterfaces.data.ICoreMailDesiredState;
|
|
15
16
|
type TCoreMailBinding = plugins.serveZoneInterfaces.data.ICoreMailBindingDesiredState;
|
|
@@ -26,6 +27,13 @@ export class CoreMailAuthenticationError extends Error {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
export class CoreMailAuthLimitError extends CoreMailAuthenticationError {
|
|
31
|
+
public constructor() {
|
|
32
|
+
super();
|
|
33
|
+
this.name = 'CoreMailAuthLimitError';
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
29
37
|
export class CoreMailAuthorizationError extends Error {
|
|
30
38
|
public constructor() {
|
|
31
39
|
super('CoreMail authority is unavailable.');
|
|
@@ -40,52 +48,154 @@ export class CoreMailCapabilityDeniedError extends Error {
|
|
|
40
48
|
}
|
|
41
49
|
}
|
|
42
50
|
|
|
51
|
+
export interface ICoreMailAuthLimiterOptions {
|
|
52
|
+
maximumInFlight?: number;
|
|
53
|
+
maximumWaiters?: number;
|
|
54
|
+
maximumWaitMs?: number;
|
|
55
|
+
identityCapacity?: number;
|
|
56
|
+
identityRefillPerSecond?: number;
|
|
57
|
+
maximumIdentities?: number;
|
|
58
|
+
now?: () => number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface IIdentityBucket {
|
|
62
|
+
tokens: number;
|
|
63
|
+
lastRefillAt: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ISlotWaiter {
|
|
67
|
+
grant: () => void;
|
|
68
|
+
expire: () => void;
|
|
69
|
+
timer: ReturnType<typeof setTimeout>;
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
export class CoreMailAuthLimiter {
|
|
73
|
+
private readonly maximumInFlight: number;
|
|
74
|
+
private readonly maximumWaiters: number;
|
|
75
|
+
private readonly maximumWaitMs: number;
|
|
76
|
+
private readonly identityCapacity: number;
|
|
77
|
+
private readonly identityRefillPerSecond: number;
|
|
78
|
+
private readonly maximumIdentities: number;
|
|
79
|
+
private readonly now: () => number;
|
|
80
|
+
|
|
44
81
|
private inFlight = 0;
|
|
45
|
-
private
|
|
46
|
-
private
|
|
82
|
+
private readonly waiters: ISlotWaiter[] = [];
|
|
83
|
+
private readonly identityBuckets = new Map<string, IIdentityBucket>();
|
|
47
84
|
|
|
48
|
-
public constructor(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
this.
|
|
85
|
+
public constructor(optionsArg: ICoreMailAuthLimiterOptions = {}) {
|
|
86
|
+
this.maximumInFlight = optionsArg.maximumInFlight ?? 4;
|
|
87
|
+
this.maximumWaiters = optionsArg.maximumWaiters ?? 64;
|
|
88
|
+
this.maximumWaitMs = optionsArg.maximumWaitMs ?? 2_500;
|
|
89
|
+
this.identityCapacity = optionsArg.identityCapacity ?? 4;
|
|
90
|
+
this.identityRefillPerSecond = optionsArg.identityRefillPerSecond ?? 1;
|
|
91
|
+
this.maximumIdentities = optionsArg.maximumIdentities ?? 4_096;
|
|
92
|
+
this.now = optionsArg.now ?? Date.now;
|
|
56
93
|
}
|
|
57
94
|
|
|
58
|
-
|
|
95
|
+
public snapshot(): ICoreMailAuthLimiterSnapshot {
|
|
96
|
+
return {
|
|
97
|
+
inFlight: this.inFlight,
|
|
98
|
+
waiting: this.waiters.length,
|
|
99
|
+
identities: this.identityBuckets.size,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
public async run<T>(
|
|
104
|
+
identityArg: string,
|
|
105
|
+
workArg: () => Promise<T>,
|
|
106
|
+
): Promise<T> {
|
|
107
|
+
this.consumeIdentityToken(identityArg);
|
|
108
|
+
await this.acquireSlot();
|
|
109
|
+
try {
|
|
110
|
+
return await workArg();
|
|
111
|
+
} finally {
|
|
112
|
+
this.releaseSlot();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private consumeIdentityToken(identityArg: string): void {
|
|
59
117
|
const now = this.now();
|
|
60
|
-
const
|
|
61
|
-
if (
|
|
118
|
+
const bucket = this.identityBuckets.get(identityArg);
|
|
119
|
+
if (!bucket) {
|
|
120
|
+
this.pruneIdentities(now);
|
|
121
|
+
this.identityBuckets.set(identityArg, {
|
|
122
|
+
tokens: this.identityCapacity - 1,
|
|
123
|
+
lastRefillAt: now,
|
|
124
|
+
});
|
|
62
125
|
return;
|
|
63
126
|
}
|
|
64
|
-
const intervals = Math.floor(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
127
|
+
const intervals = Math.floor(Math.max(0, now - bucket.lastRefillAt) / 1_000);
|
|
128
|
+
if (intervals > 0) {
|
|
129
|
+
bucket.tokens = Math.min(
|
|
130
|
+
this.identityCapacity,
|
|
131
|
+
bucket.tokens + (intervals * this.identityRefillPerSecond),
|
|
132
|
+
);
|
|
133
|
+
bucket.lastRefillAt += intervals * 1_000;
|
|
134
|
+
}
|
|
135
|
+
if (bucket.tokens < 1) {
|
|
136
|
+
throw new CoreMailAuthLimitError();
|
|
137
|
+
}
|
|
138
|
+
bucket.tokens--;
|
|
70
139
|
}
|
|
71
140
|
|
|
72
|
-
|
|
73
|
-
this.
|
|
74
|
-
|
|
141
|
+
private pruneIdentities(nowArg: number): void {
|
|
142
|
+
if (this.identityBuckets.size < this.maximumIdentities) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
for (const [identity, bucket] of this.identityBuckets) {
|
|
146
|
+
const refilled = bucket.tokens + (
|
|
147
|
+
Math.floor(Math.max(0, nowArg - bucket.lastRefillAt) / 1_000)
|
|
148
|
+
* this.identityRefillPerSecond
|
|
149
|
+
);
|
|
150
|
+
if (refilled >= this.identityCapacity) {
|
|
151
|
+
this.identityBuckets.delete(identity);
|
|
152
|
+
if (this.identityBuckets.size < this.maximumIdentities) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const identity of this.identityBuckets.keys()) {
|
|
158
|
+
this.identityBuckets.delete(identity);
|
|
159
|
+
if (this.identityBuckets.size < this.maximumIdentities) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
75
163
|
}
|
|
76
164
|
|
|
77
|
-
|
|
78
|
-
this.
|
|
79
|
-
|
|
80
|
-
|
|
165
|
+
private async acquireSlot(): Promise<void> {
|
|
166
|
+
if (this.inFlight < this.maximumInFlight) {
|
|
167
|
+
this.inFlight++;
|
|
168
|
+
return;
|
|
81
169
|
}
|
|
82
|
-
this.
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
return await workArg();
|
|
86
|
-
} finally {
|
|
87
|
-
this.inFlight--;
|
|
170
|
+
if (this.waiters.length >= this.maximumWaiters) {
|
|
171
|
+
throw new CoreMailAuthLimitError();
|
|
88
172
|
}
|
|
173
|
+
await new Promise<void>((resolveArg, rejectArg) => {
|
|
174
|
+
const waiter: ISlotWaiter = {
|
|
175
|
+
grant: () => {
|
|
176
|
+
clearTimeout(waiter.timer);
|
|
177
|
+
resolveArg();
|
|
178
|
+
},
|
|
179
|
+
expire: () => {
|
|
180
|
+
const index = this.waiters.indexOf(waiter);
|
|
181
|
+
if (index >= 0) {
|
|
182
|
+
this.waiters.splice(index, 1);
|
|
183
|
+
}
|
|
184
|
+
rejectArg(new CoreMailAuthLimitError());
|
|
185
|
+
},
|
|
186
|
+
timer: setTimeout(() => waiter.expire(), this.maximumWaitMs),
|
|
187
|
+
};
|
|
188
|
+
this.waiters.push(waiter);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private releaseSlot(): void {
|
|
193
|
+
const waiter = this.waiters.shift();
|
|
194
|
+
if (waiter) {
|
|
195
|
+
waiter.grant();
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
this.inFlight--;
|
|
89
199
|
}
|
|
90
200
|
}
|
|
91
201
|
|
|
@@ -108,7 +218,7 @@ const findAcceptedVerifier = (
|
|
|
108
218
|
);
|
|
109
219
|
|
|
110
220
|
export class CoreMailAuth {
|
|
111
|
-
public readonly limiter
|
|
221
|
+
public readonly limiter: CoreMailAuthLimiter;
|
|
112
222
|
|
|
113
223
|
public constructor(
|
|
114
224
|
private readonly controlBootstrap:
|
|
@@ -116,7 +226,9 @@ export class CoreMailAuth {
|
|
|
116
226
|
private readonly getActiveDesiredState: () =>
|
|
117
227
|
Promise<TCoreMailDesiredState | null>,
|
|
118
228
|
private readonly now: () => number = Date.now,
|
|
119
|
-
) {
|
|
229
|
+
) {
|
|
230
|
+
this.limiter = new CoreMailAuthLimiter({ now: this.now });
|
|
231
|
+
}
|
|
120
232
|
|
|
121
233
|
private requireFreshPeer(peerArg: ICoreMailPeer): void {
|
|
122
234
|
const deadline = peerArg.context.state[coreMailPreAuthDeadlineKey];
|
|
@@ -130,19 +242,23 @@ export class CoreMailAuth {
|
|
|
130
242
|
}
|
|
131
243
|
}
|
|
132
244
|
|
|
133
|
-
private
|
|
134
|
-
verifierArg: TCoreMailVerifier | undefined,
|
|
135
|
-
secretArg: string,
|
|
136
|
-
): Promise<void> {
|
|
245
|
+
private requireSecretShape(secretArg: unknown): string {
|
|
137
246
|
if (
|
|
138
|
-
|
|
139
|
-
|| typeof secretArg !== 'string'
|
|
247
|
+
typeof secretArg !== 'string'
|
|
140
248
|
|| secretArg.length < 32
|
|
141
249
|
|| secretArg.length > 512
|
|
142
250
|
) {
|
|
143
251
|
throw new CoreMailAuthenticationError();
|
|
144
252
|
}
|
|
145
|
-
|
|
253
|
+
return secretArg;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private async verify(
|
|
257
|
+
verifierArg: TCoreMailVerifier,
|
|
258
|
+
secretArg: string,
|
|
259
|
+
identityArg: string,
|
|
260
|
+
): Promise<void> {
|
|
261
|
+
const verified = await this.limiter.run(identityArg, async () =>
|
|
146
262
|
plugins.argon2.verify(verifierArg.verificationHash, secretArg)
|
|
147
263
|
);
|
|
148
264
|
if (!verified) {
|
|
@@ -156,8 +272,10 @@ export class CoreMailAuth {
|
|
|
156
272
|
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailAuthenticateControl['request'],
|
|
157
273
|
): Promise<ICoreMailControlSession> {
|
|
158
274
|
this.requireFreshPeer(peerArg);
|
|
275
|
+
let failureCode = 'INVALID_INPUT';
|
|
276
|
+
let credentialId: string | undefined;
|
|
159
277
|
try {
|
|
160
|
-
|
|
278
|
+
credentialId = requireCoreMailIdentifier(
|
|
161
279
|
requestArg.credentialId,
|
|
162
280
|
'credentialId',
|
|
163
281
|
);
|
|
@@ -166,13 +284,19 @@ export class CoreMailAuth {
|
|
|
166
284
|
'credentialVersion',
|
|
167
285
|
1,
|
|
168
286
|
);
|
|
287
|
+
const credentialSecret = this.requireSecretShape(requestArg.credentialSecret);
|
|
288
|
+
failureCode = 'UNKNOWN_CREDENTIAL';
|
|
169
289
|
const verifier = findAcceptedVerifier(
|
|
170
290
|
this.controlBootstrap.credentials,
|
|
171
291
|
credentialId,
|
|
172
292
|
credentialVersion,
|
|
173
293
|
this.now(),
|
|
174
294
|
);
|
|
175
|
-
|
|
295
|
+
if (!verifier) {
|
|
296
|
+
throw new CoreMailAuthenticationError();
|
|
297
|
+
}
|
|
298
|
+
failureCode = 'VERIFIER_REJECTED';
|
|
299
|
+
await this.verify(verifier, credentialSecret, `control:${credentialId}`);
|
|
176
300
|
const session = Object.freeze<ICoreMailControlSession>({
|
|
177
301
|
kind: 'control',
|
|
178
302
|
peerId: peerArg.id,
|
|
@@ -182,8 +306,17 @@ export class CoreMailAuth {
|
|
|
182
306
|
});
|
|
183
307
|
peerArg.data.set(coreMailSessionKey, session);
|
|
184
308
|
return session;
|
|
185
|
-
} catch {
|
|
309
|
+
} catch (error) {
|
|
186
310
|
peerArg.close(4401, 'authentication failed');
|
|
311
|
+
await logCoreMailFailure(
|
|
312
|
+
'auth',
|
|
313
|
+
error instanceof CoreMailAuthLimitError ? 'LIMITER_EXHAUSTED' : failureCode,
|
|
314
|
+
undefined,
|
|
315
|
+
{
|
|
316
|
+
kind: 'control',
|
|
317
|
+
...(credentialId === undefined ? {} : { credentialId }),
|
|
318
|
+
},
|
|
319
|
+
).catch(() => undefined);
|
|
187
320
|
throw new CoreMailAuthenticationError();
|
|
188
321
|
}
|
|
189
322
|
}
|
|
@@ -194,13 +327,16 @@ export class CoreMailAuth {
|
|
|
194
327
|
plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailAuthenticateWorkload['request'],
|
|
195
328
|
): Promise<ICoreMailWorkloadSession> {
|
|
196
329
|
this.requireFreshPeer(peerArg);
|
|
330
|
+
let failureCode = 'INVALID_INPUT';
|
|
331
|
+
let bindingId: string | undefined;
|
|
332
|
+
let credentialId: string | undefined;
|
|
197
333
|
try {
|
|
198
334
|
const desiredState = await this.getActiveDesiredState();
|
|
199
|
-
|
|
335
|
+
bindingId = requireCoreMailIdentifier(
|
|
200
336
|
requestArg.bindingId,
|
|
201
337
|
'bindingId',
|
|
202
338
|
);
|
|
203
|
-
|
|
339
|
+
credentialId = requireCoreMailIdentifier(
|
|
204
340
|
requestArg.credentialId,
|
|
205
341
|
'credentialId',
|
|
206
342
|
);
|
|
@@ -209,44 +345,64 @@ export class CoreMailAuth {
|
|
|
209
345
|
'credentialVersion',
|
|
210
346
|
1,
|
|
211
347
|
);
|
|
348
|
+
const credentialSecret = this.requireSecretShape(requestArg.credentialSecret);
|
|
349
|
+
failureCode = 'UNKNOWN_BINDING';
|
|
212
350
|
const binding = desiredState?.bindings.find((bindingArg) =>
|
|
213
351
|
bindingArg.bindingId === bindingId
|
|
214
352
|
&& bindingArg.state !== 'disabled'
|
|
215
353
|
);
|
|
216
|
-
|
|
354
|
+
if (!desiredState || !binding || binding.state === 'disabled') {
|
|
355
|
+
throw new CoreMailAuthenticationError();
|
|
356
|
+
}
|
|
357
|
+
failureCode = 'UNKNOWN_CREDENTIAL';
|
|
358
|
+
const verifier = findAcceptedVerifier(
|
|
217
359
|
binding.credentials,
|
|
218
360
|
credentialId,
|
|
219
361
|
credentialVersion,
|
|
220
362
|
this.now(),
|
|
221
363
|
);
|
|
222
|
-
|
|
364
|
+
if (!verifier) {
|
|
365
|
+
throw new CoreMailAuthenticationError();
|
|
366
|
+
}
|
|
367
|
+
failureCode = 'VERIFIER_REJECTED';
|
|
368
|
+
await this.verify(verifier, credentialSecret, `workload:${bindingId}`);
|
|
223
369
|
const allowedOperations =
|
|
224
370
|
plugins.serveZoneInterfaces.data.resolveCoreMailWorkloadOperations(
|
|
225
|
-
binding
|
|
226
|
-
binding
|
|
371
|
+
binding.state,
|
|
372
|
+
binding.capabilities,
|
|
227
373
|
);
|
|
228
374
|
const session = Object.freeze<ICoreMailWorkloadSession>({
|
|
229
375
|
kind: 'workload',
|
|
230
376
|
peerId: peerArg.id,
|
|
231
|
-
tenantId: binding
|
|
232
|
-
serviceId: binding
|
|
233
|
-
bindingId: binding
|
|
234
|
-
bindingRevision: binding
|
|
235
|
-
configEpoch: desiredState
|
|
377
|
+
tenantId: binding.tenantId,
|
|
378
|
+
serviceId: binding.serviceId,
|
|
379
|
+
bindingId: binding.bindingId,
|
|
380
|
+
bindingRevision: binding.revision,
|
|
381
|
+
configEpoch: desiredState.configEpoch,
|
|
236
382
|
credentialId,
|
|
237
383
|
credentialVersion,
|
|
238
|
-
bindingState: binding
|
|
239
|
-
capabilities: Object.freeze([...binding
|
|
384
|
+
bindingState: binding.state,
|
|
385
|
+
capabilities: Object.freeze([...binding.capabilities]) as
|
|
240
386
|
plugins.serveZoneInterfaces.data.TCoreMailCapability[],
|
|
241
387
|
allowedOperations: Object.freeze([...allowedOperations]) as
|
|
242
388
|
plugins.serveZoneInterfaces.data.TCoreMailWorkloadOperation[],
|
|
243
|
-
coreMailTransferOrigin: desiredState
|
|
389
|
+
coreMailTransferOrigin: desiredState.gateway.coreMailTransferOrigin,
|
|
244
390
|
authenticatedAt: this.now(),
|
|
245
391
|
});
|
|
246
392
|
peerArg.data.set(coreMailSessionKey, session);
|
|
247
393
|
return session;
|
|
248
|
-
} catch {
|
|
394
|
+
} catch (error) {
|
|
249
395
|
peerArg.close(4401, 'authentication failed');
|
|
396
|
+
await logCoreMailFailure(
|
|
397
|
+
'auth',
|
|
398
|
+
error instanceof CoreMailAuthLimitError ? 'LIMITER_EXHAUSTED' : failureCode,
|
|
399
|
+
undefined,
|
|
400
|
+
{
|
|
401
|
+
kind: 'workload',
|
|
402
|
+
...(bindingId === undefined ? {} : { bindingId }),
|
|
403
|
+
...(credentialId === undefined ? {} : { credentialId }),
|
|
404
|
+
},
|
|
405
|
+
).catch(() => undefined);
|
|
250
406
|
throw new CoreMailAuthenticationError();
|
|
251
407
|
}
|
|
252
408
|
}
|
package/ts/classes.config.ts
CHANGED
|
@@ -3,8 +3,13 @@ import type { ICoreMailConfig } from './interfaces.js';
|
|
|
3
3
|
|
|
4
4
|
const requireEnv = (keyArg: string): string => {
|
|
5
5
|
const value = process.env[keyArg];
|
|
6
|
-
if (!value
|
|
7
|
-
throw new Error(`CoreMail requires
|
|
6
|
+
if (!value) {
|
|
7
|
+
throw new Error(`CoreMail requires ${keyArg} to be set and non-empty.`);
|
|
8
|
+
}
|
|
9
|
+
if (value.trim() !== value) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
`CoreMail requires ${keyArg} to be canonical without surrounding whitespace.`,
|
|
12
|
+
);
|
|
8
13
|
}
|
|
9
14
|
return value;
|
|
10
15
|
};
|
|
@@ -145,8 +145,8 @@ export class CoreMailDesiredStateService {
|
|
|
145
145
|
modelArg.state = 'active';
|
|
146
146
|
modelArg.activatedAt = stateArg.updatedAt;
|
|
147
147
|
},
|
|
148
|
-
}).catch(async () => {
|
|
149
|
-
await logCoreMailFailure('desired-state', 'SNAPSHOT_REPAIR_FAILED')
|
|
148
|
+
}).catch(async (error) => {
|
|
149
|
+
await logCoreMailFailure('desired-state', 'SNAPSHOT_REPAIR_FAILED', error)
|
|
150
150
|
.catch(() => undefined);
|
|
151
151
|
});
|
|
152
152
|
}
|
package/ts/classes.gateway.ts
CHANGED
|
@@ -27,6 +27,21 @@ const gatewayAuthority = (serviceIdArg: string): ITransferAuthority => ({
|
|
|
27
27
|
bindingId: '-',
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
export const stableStringify = (valueArg: unknown): string => {
|
|
31
|
+
if (Array.isArray(valueArg)) {
|
|
32
|
+
return `[${valueArg.map(stableStringify).join(',')}]`;
|
|
33
|
+
}
|
|
34
|
+
if (valueArg && typeof valueArg === 'object') {
|
|
35
|
+
const record = valueArg as Record<string, unknown>;
|
|
36
|
+
return `{${Object.keys(record)
|
|
37
|
+
.filter((keyArg) => record[keyArg] !== undefined)
|
|
38
|
+
.sort()
|
|
39
|
+
.map((keyArg) => `${JSON.stringify(keyArg)}:${stableStringify(record[keyArg])}`)
|
|
40
|
+
.join(',')}}`;
|
|
41
|
+
}
|
|
42
|
+
return JSON.stringify(valueArg) ?? 'null';
|
|
43
|
+
};
|
|
44
|
+
|
|
30
45
|
const normalizeTransferOrigin = (valueArg: unknown): string => {
|
|
31
46
|
if (typeof valueArg !== 'string') {
|
|
32
47
|
throw new Error('CoreMail gateway returned an invalid transfer origin.');
|
|
@@ -138,7 +153,7 @@ export class CoreMailGateway {
|
|
|
138
153
|
if (!gateway) {
|
|
139
154
|
await this.disconnect();
|
|
140
155
|
} else {
|
|
141
|
-
const signature =
|
|
156
|
+
const signature = stableStringify(gateway);
|
|
142
157
|
if (
|
|
143
158
|
!this.socket
|
|
144
159
|
|| signature !== this.socketSignature
|
|
@@ -152,8 +167,8 @@ export class CoreMailGateway {
|
|
|
152
167
|
this.queueAuthentication(gateway, signature);
|
|
153
168
|
}
|
|
154
169
|
}
|
|
155
|
-
} catch {
|
|
156
|
-
await logCoreMailFailure('gateway', 'RECONCILIATION_FAILED')
|
|
170
|
+
} catch (error) {
|
|
171
|
+
await logCoreMailFailure('gateway', 'RECONCILIATION_FAILED', error)
|
|
157
172
|
.catch(() => undefined);
|
|
158
173
|
await this.disconnect();
|
|
159
174
|
}
|
|
@@ -264,12 +279,12 @@ export class CoreMailGateway {
|
|
|
264
279
|
this.transferOrigin = transferOrigin;
|
|
265
280
|
this.authenticatedSignature = signatureArg;
|
|
266
281
|
})
|
|
267
|
-
.catch(async () => {
|
|
282
|
+
.catch(async (error) => {
|
|
268
283
|
if (this.socketSignature === signatureArg) {
|
|
269
284
|
this.authenticatedSignature = '';
|
|
270
285
|
this.transferOrigin = null;
|
|
271
286
|
}
|
|
272
|
-
await logCoreMailFailure('gateway', 'AUTHENTICATION_FAILED')
|
|
287
|
+
await logCoreMailFailure('gateway', 'AUTHENTICATION_FAILED', error)
|
|
273
288
|
.catch(() => undefined);
|
|
274
289
|
})
|
|
275
290
|
.finally(() => {
|
|
@@ -596,8 +611,8 @@ export class CoreMailOutboundWorker {
|
|
|
596
611
|
private async run(): Promise<void> {
|
|
597
612
|
while (!this.stopController.signal.aborted) {
|
|
598
613
|
if (this.gateway.isReady()) {
|
|
599
|
-
await this.processBatch().catch(async () => {
|
|
600
|
-
await logCoreMailFailure('worker', 'BATCH_FAILED')
|
|
614
|
+
await this.processBatch().catch(async (error) => {
|
|
615
|
+
await logCoreMailFailure('worker', 'BATCH_FAILED', error)
|
|
601
616
|
.catch(() => undefined);
|
|
602
617
|
});
|
|
603
618
|
}
|
|
@@ -742,9 +757,10 @@ export class CoreMailOutboundWorker {
|
|
|
742
757
|
response.status,
|
|
743
758
|
leaseTokenArg,
|
|
744
759
|
);
|
|
745
|
-
} catch {
|
|
746
|
-
await logCoreMailFailure('worker', 'DELIVERY_ATTEMPT_FAILED'
|
|
747
|
-
.
|
|
760
|
+
} catch (error) {
|
|
761
|
+
await logCoreMailFailure('worker', 'DELIVERY_ATTEMPT_FAILED', error, {
|
|
762
|
+
submissionId: submissionArg.submissionId,
|
|
763
|
+
}).catch(() => undefined);
|
|
748
764
|
const delayMs = Math.min(
|
|
749
765
|
15 * 60 * 1000,
|
|
750
766
|
1_000 * (2 ** Math.min(submissionArg.attempts, 9)),
|
|
@@ -315,9 +315,10 @@ export class CoreMailMaintenanceService {
|
|
|
315
315
|
if (this.sweepTask) return await this.sweepTask;
|
|
316
316
|
const controller = new AbortController();
|
|
317
317
|
this.sweepController = controller;
|
|
318
|
-
const task = this.sweep(controller.signal).catch(async () => {
|
|
318
|
+
const task = this.sweep(controller.signal).catch(async (error) => {
|
|
319
319
|
if (!controller.signal.aborted) {
|
|
320
|
-
await logCoreMailFailure('retention', 'SWEEP_FAILED'
|
|
320
|
+
await logCoreMailFailure('retention', 'SWEEP_FAILED', error)
|
|
321
|
+
.catch(() => undefined);
|
|
321
322
|
}
|
|
322
323
|
});
|
|
323
324
|
this.sweepTask = task;
|
package/ts/classes.server.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
ICoreMailWorkloadSession,
|
|
21
21
|
} from './interfaces.js';
|
|
22
22
|
import { CoreMailQuotaExceededError } from './coremail.quota.js';
|
|
23
|
+
import { logCoreMailFailure } from './coremail.log.js';
|
|
23
24
|
|
|
24
25
|
type TErrorCode = plugins.serveZoneInterfaces.data.TCoreMailErrorCode;
|
|
25
26
|
|
|
@@ -425,14 +426,21 @@ export class CoreMailServer {
|
|
|
425
426
|
});
|
|
426
427
|
}
|
|
427
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Register one typed handler with uniform failure redaction.
|
|
431
|
+
*
|
|
432
|
+
* `handlerArg` is typed as typedrequest's own `THandlerFunction` rather than
|
|
433
|
+
* a hand-written signature: since typedrequest 8 both sides are wrapped in
|
|
434
|
+
* the `TReverseVirtualStreamDirections` mapped type, which does not reduce
|
|
435
|
+
* inside a generic wrapper, so any restatement of the shape stops being
|
|
436
|
+
* assignable. Concrete `IReq_*` types reduce normally, which is why the call
|
|
437
|
+
* sites below need no change.
|
|
438
|
+
*/
|
|
428
439
|
private addHandler<
|
|
429
440
|
TRequest extends plugins.typedrequestInterfaces.ITypedRequest,
|
|
430
441
|
>(
|
|
431
442
|
methodArg: TRequest['method'],
|
|
432
|
-
handlerArg:
|
|
433
|
-
requestArg: TRequest['request'],
|
|
434
|
-
toolsArg: ICoreMailTypedTools | undefined,
|
|
435
|
-
) => Promise<TRequest['response']>,
|
|
443
|
+
handlerArg: plugins.typedrequest.THandlerFunction<TRequest>,
|
|
436
444
|
): void {
|
|
437
445
|
this.router.addTypedHandler(new plugins.typedrequest.TypedHandler<TRequest>(
|
|
438
446
|
methodArg,
|
|
@@ -440,7 +448,13 @@ export class CoreMailServer {
|
|
|
440
448
|
try {
|
|
441
449
|
return await handlerArg(requestArg, toolsArg);
|
|
442
450
|
} catch (error) {
|
|
443
|
-
|
|
451
|
+
const safeError = privacySafeError(error);
|
|
452
|
+
if (safeError.errorData?.code === 'INVALID_REQUEST') {
|
|
453
|
+
await logCoreMailFailure('server', 'UNEXPECTED_HANDLER_FAILURE', error, {
|
|
454
|
+
method: methodArg,
|
|
455
|
+
}).catch(() => undefined);
|
|
456
|
+
}
|
|
457
|
+
throw safeError;
|
|
444
458
|
}
|
|
445
459
|
},
|
|
446
460
|
));
|