@serve.zone/coremail 1.0.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.
Files changed (75) hide show
  1. package/.smartconfig.json +50 -0
  2. package/changelog.md +10 -0
  3. package/cli.js +4 -0
  4. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  5. package/dist_ts/00_commitinfo_data.js +9 -0
  6. package/dist_ts/classes.auth.d.ts +45 -0
  7. package/dist_ts/classes.auth.js +204 -0
  8. package/dist_ts/classes.config.d.ts +2 -0
  9. package/dist_ts/classes.config.js +72 -0
  10. package/dist_ts/classes.coremail.d.ts +41 -0
  11. package/dist_ts/classes.coremail.js +155 -0
  12. package/dist_ts/classes.desiredstate.d.ts +40 -0
  13. package/dist_ts/classes.desiredstate.js +382 -0
  14. package/dist_ts/classes.gateway.d.ts +62 -0
  15. package/dist_ts/classes.gateway.js +669 -0
  16. package/dist_ts/classes.inbound.d.ts +49 -0
  17. package/dist_ts/classes.inbound.js +702 -0
  18. package/dist_ts/classes.maintenance.d.ts +16 -0
  19. package/dist_ts/classes.maintenance.js +339 -0
  20. package/dist_ts/classes.models.d.ts +161 -0
  21. package/dist_ts/classes.models.js +595 -0
  22. package/dist_ts/classes.server.d.ts +40 -0
  23. package/dist_ts/classes.server.js +309 -0
  24. package/dist_ts/classes.storage.d.ts +32 -0
  25. package/dist_ts/classes.storage.js +317 -0
  26. package/dist_ts/classes.submissions.d.ts +33 -0
  27. package/dist_ts/classes.submissions.js +385 -0
  28. package/dist_ts/classes.transfer.d.ts +41 -0
  29. package/dist_ts/classes.transfer.js +289 -0
  30. package/dist_ts/coremail.cursor.d.ts +20 -0
  31. package/dist_ts/coremail.cursor.js +191 -0
  32. package/dist_ts/coremail.log.d.ts +3 -0
  33. package/dist_ts/coremail.log.js +11 -0
  34. package/dist_ts/coremail.mime.d.ts +8 -0
  35. package/dist_ts/coremail.mime.js +63 -0
  36. package/dist_ts/coremail.persistence.d.ts +148 -0
  37. package/dist_ts/coremail.persistence.js +695 -0
  38. package/dist_ts/coremail.quota.d.ts +29 -0
  39. package/dist_ts/coremail.quota.js +149 -0
  40. package/dist_ts/coremail.selectors.d.ts +3 -0
  41. package/dist_ts/coremail.selectors.js +21 -0
  42. package/dist_ts/coremail.validation.d.ts +9 -0
  43. package/dist_ts/coremail.validation.js +325 -0
  44. package/dist_ts/index.d.ts +4 -0
  45. package/dist_ts/index.js +39 -0
  46. package/dist_ts/interfaces.d.ts +92 -0
  47. package/dist_ts/interfaces.js +2 -0
  48. package/dist_ts/plugins.d.ts +11 -0
  49. package/dist_ts/plugins.js +12 -0
  50. package/license.md +19 -0
  51. package/package.json +48 -0
  52. package/readme.md +199 -0
  53. package/ts/00_commitinfo_data.ts +8 -0
  54. package/ts/classes.auth.ts +321 -0
  55. package/ts/classes.config.ts +104 -0
  56. package/ts/classes.coremail.ts +249 -0
  57. package/ts/classes.desiredstate.ts +508 -0
  58. package/ts/classes.gateway.ts +811 -0
  59. package/ts/classes.inbound.ts +898 -0
  60. package/ts/classes.maintenance.ts +335 -0
  61. package/ts/classes.models.ts +530 -0
  62. package/ts/classes.server.ts +444 -0
  63. package/ts/classes.storage.ts +448 -0
  64. package/ts/classes.submissions.ts +554 -0
  65. package/ts/classes.transfer.ts +379 -0
  66. package/ts/coremail.cursor.ts +328 -0
  67. package/ts/coremail.log.ts +21 -0
  68. package/ts/coremail.mime.ts +94 -0
  69. package/ts/coremail.persistence.ts +1156 -0
  70. package/ts/coremail.quota.ts +214 -0
  71. package/ts/coremail.selectors.ts +35 -0
  72. package/ts/coremail.validation.ts +446 -0
  73. package/ts/index.ts +38 -0
  74. package/ts/interfaces.ts +109 -0
  75. package/ts/plugins.ts +11 -0
@@ -0,0 +1,444 @@
1
+ import * as plugins from './plugins.js';
2
+ import {
3
+ CoreMailAuthenticationError,
4
+ CoreMailAuthorizationError,
5
+ CoreMailCapabilityDeniedError,
6
+ CoreMailAuth,
7
+ coreMailPreAuthDeadlineKey,
8
+ coreMailSessionKey,
9
+ } from './classes.auth.js';
10
+ import type { CoreMailDesiredStateService } from './classes.desiredstate.js';
11
+ import type { CoreMailGateway } from './classes.gateway.js';
12
+ import type { CoreMailInboundService } from './classes.inbound.js';
13
+ import type { CoreMailStorage } from './classes.storage.js';
14
+ import type { CoreMailSubmissionService } from './classes.submissions.js';
15
+ import type { CoreMailTransferService } from './classes.transfer.js';
16
+ import type {
17
+ ICoreMailConfig,
18
+ ICoreMailPeer,
19
+ ICoreMailReadiness,
20
+ ICoreMailWorkloadSession,
21
+ } from './interfaces.js';
22
+ import { CoreMailQuotaExceededError } from './coremail.quota.js';
23
+
24
+ type TErrorCode = plugins.serveZoneInterfaces.data.TCoreMailErrorCode;
25
+
26
+ interface ICoreMailTypedTools {
27
+ localData: Record<string, unknown>;
28
+ }
29
+
30
+ const privacySafeError = (
31
+ errorArg: unknown,
32
+ ): plugins.typedrequest.TypedResponseError => {
33
+ let code: TErrorCode = 'INVALID_REQUEST';
34
+ let retryable = false;
35
+ if (errorArg instanceof CoreMailAuthenticationError) {
36
+ code = 'AUTHENTICATION_FAILED';
37
+ } else if (errorArg instanceof CoreMailCapabilityDeniedError) {
38
+ code = 'CAPABILITY_DENIED';
39
+ } else if (errorArg instanceof CoreMailAuthorizationError) {
40
+ code = 'AUTHORITY_REVOKED';
41
+ } else if (errorArg instanceof CoreMailQuotaExceededError) {
42
+ return new plugins.typedrequest.TypedResponseError('CoreMail request rejected.', {
43
+ code: 'QUOTA_EXCEEDED',
44
+ retryable: true,
45
+ ...(errorArg.retryAfterMs === undefined
46
+ ? {}
47
+ : { retryAfterMs: errorArg.retryAfterMs }),
48
+ } satisfies plugins.serveZoneInterfaces.data.ICoreMailErrorData);
49
+ } else if (
50
+ errorArg instanceof Error
51
+ && errorArg.message.includes('gateway')
52
+ && errorArg.message.includes('unavailable')
53
+ ) {
54
+ code = 'GATEWAY_UNAVAILABLE';
55
+ retryable = true;
56
+ } else if (
57
+ errorArg instanceof Error
58
+ && errorArg.message.includes('idempotency')
59
+ ) {
60
+ code = 'IDEMPOTENCY_CONFLICT';
61
+ } else if (
62
+ errorArg instanceof Error
63
+ && errorArg.message.includes('fence')
64
+ ) {
65
+ code = 'STATE_CONFLICT';
66
+ retryable = true;
67
+ } else if (
68
+ errorArg instanceof Error
69
+ && (
70
+ errorArg.message.includes('byte budget')
71
+ || errorArg.message.includes('content budget')
72
+ || errorArg.message.includes('exceeds')
73
+ )
74
+ ) {
75
+ code = 'PAYLOAD_LIMIT_EXCEEDED';
76
+ }
77
+ return new plugins.typedrequest.TypedResponseError('CoreMail request rejected.', {
78
+ code,
79
+ retryable,
80
+ } satisfies plugins.serveZoneInterfaces.data.ICoreMailErrorData);
81
+ };
82
+
83
+ const asPeer = (
84
+ toolsArg: ICoreMailTypedTools | undefined,
85
+ ): ICoreMailPeer => {
86
+ const peer = toolsArg?.localData?.peer as ICoreMailPeer | undefined;
87
+ if (
88
+ !peer
89
+ || typeof peer.id !== 'string'
90
+ || !(peer.data instanceof Map)
91
+ || typeof peer.close !== 'function'
92
+ || typeof peer.terminate !== 'function'
93
+ ) {
94
+ throw new CoreMailAuthenticationError();
95
+ }
96
+ return peer;
97
+ };
98
+
99
+ export const countCoreMailActiveSessions = (
100
+ sessionsArg: Iterable<ICoreMailWorkloadSession | undefined>,
101
+ bindingArg: plugins.serveZoneInterfaces.data.ICoreMailBindingDesiredState,
102
+ configEpochArg: number,
103
+ ): Array<{ credentialId: string; version: number; count: number }> => {
104
+ const counts = new Map<
105
+ string,
106
+ { credentialId: string; version: number; count: number }
107
+ >();
108
+ for (const session of sessionsArg) {
109
+ if (
110
+ session?.kind === 'workload'
111
+ && session.tenantId === bindingArg.tenantId
112
+ && session.serviceId === bindingArg.serviceId
113
+ && session.bindingId === bindingArg.bindingId
114
+ && session.bindingRevision === bindingArg.revision
115
+ && session.configEpoch === configEpochArg
116
+ ) {
117
+ const key = `${session.credentialId}\u0000${session.credentialVersion}`;
118
+ const count = counts.get(key);
119
+ counts.set(key, count
120
+ ? { ...count, count: count.count + 1 }
121
+ : {
122
+ credentialId: session.credentialId,
123
+ version: session.credentialVersion,
124
+ count: 1,
125
+ });
126
+ }
127
+ }
128
+ return [...counts.values()].sort((leftArg, rightArg) =>
129
+ leftArg.version - rightArg.version
130
+ || leftArg.credentialId.localeCompare(rightArg.credentialId)
131
+ );
132
+ };
133
+
134
+ export class CoreMailServer {
135
+ public readonly router:
136
+ plugins.typedrequest.TypedRouter<plugins.typedrequestInterfaces.ITypedRequest> =
137
+ new plugins.typedrequest.TypedRouter();
138
+ public readonly typedServer: plugins.typedserver.TypedServer;
139
+ private preAuthSweepTimer: ReturnType<typeof setInterval> | null = null;
140
+
141
+ public constructor(
142
+ private readonly config: ICoreMailConfig,
143
+ private readonly database: plugins.smartdata.SmartdataDb,
144
+ private readonly storage: CoreMailStorage,
145
+ private readonly transfers: CoreMailTransferService,
146
+ private readonly desiredState: CoreMailDesiredStateService,
147
+ private readonly auth: CoreMailAuth,
148
+ private readonly submissions: CoreMailSubmissionService,
149
+ private readonly inbound: CoreMailInboundService,
150
+ private readonly gateway: CoreMailGateway,
151
+ private readonly now: () => number = Date.now,
152
+ ) {
153
+ this.registerHandlers();
154
+ this.typedServer = new plugins.typedserver.TypedServer({
155
+ port: config.port,
156
+ cors: false,
157
+ authorityValidation: 'strict',
158
+ connectionTimeout: 15_000,
159
+ headersTimeout: 10_000,
160
+ requestTimeout: 2 * 60_000,
161
+ websocketMaxPayloadBytes:
162
+ plugins.serveZoneInterfaces.data.coreMailLimits.controlMessageBytes,
163
+ healthzEndpoint: false,
164
+ noCache: true,
165
+ compression: false,
166
+ surfaces: [{
167
+ name: 'coremail',
168
+ match: {
169
+ hostnames: config.hostnames,
170
+ pathPrefixes: ['/'],
171
+ websocketPathPrefixes: ['/socket'],
172
+ },
173
+ typedRequestPath: false,
174
+ websocketTypedRouter: this.router,
175
+ cors: false,
176
+ noCache: true,
177
+ includeBuiltinTypedHandlers: false,
178
+ websocketAdmission: (contextArg) => {
179
+ if (contextArg.path !== '/socket') {
180
+ return new Response('Not Found', { status: 404 });
181
+ }
182
+ contextArg.state[coreMailPreAuthDeadlineKey] = this.now() + 5_000;
183
+ return true;
184
+ },
185
+ httpHandler: async (contextArg) => {
186
+ const transferResponse = await this.transfers.handleHttp(contextArg);
187
+ if (transferResponse) {
188
+ return transferResponse;
189
+ }
190
+ if (
191
+ (contextArg.method === 'GET' || contextArg.method === 'HEAD')
192
+ && contextArg.path === '/live'
193
+ ) {
194
+ return new Response(
195
+ contextArg.method === 'HEAD' ? null : JSON.stringify({ live: true }),
196
+ {
197
+ status: 200,
198
+ headers: {
199
+ 'Content-Type': 'application/json',
200
+ 'Cache-Control': 'no-store',
201
+ },
202
+ },
203
+ );
204
+ }
205
+ if (
206
+ (contextArg.method === 'GET' || contextArg.method === 'HEAD')
207
+ && contextArg.path === '/ready'
208
+ ) {
209
+ const readiness = await this.getReadiness();
210
+ const ready = Object.values(readiness).every(Boolean);
211
+ return new Response(
212
+ contextArg.method === 'HEAD' ? null : JSON.stringify(readiness),
213
+ {
214
+ status: ready ? 200 : 503,
215
+ headers: {
216
+ 'Content-Type': 'application/json',
217
+ 'Cache-Control': 'no-store',
218
+ },
219
+ },
220
+ );
221
+ }
222
+ return null;
223
+ },
224
+ securityHeaders: {
225
+ xFrameOptions: 'DENY',
226
+ xContentTypeOptions: true,
227
+ referrerPolicy: 'no-referrer',
228
+ permissionsPolicy: {},
229
+ crossOriginOpenerPolicy: 'same-origin',
230
+ crossOriginResourcePolicy: 'same-origin',
231
+ },
232
+ }],
233
+ });
234
+ }
235
+
236
+ public async start(): Promise<void> {
237
+ await this.typedServer.start();
238
+ this.preAuthSweepTimer = setInterval(() => {
239
+ const now = this.now();
240
+ for (const peer of this.typedServer.smartServe.getWebSocketConnections()) {
241
+ const deadline = Number(peer.context.state[coreMailPreAuthDeadlineKey]);
242
+ if (
243
+ !peer.data.has(coreMailSessionKey)
244
+ && (!Number.isSafeInteger(deadline) || deadline < now)
245
+ ) {
246
+ peer.terminate();
247
+ }
248
+ }
249
+ }, 250);
250
+ this.preAuthSweepTimer.unref?.();
251
+ }
252
+
253
+ public async stop(): Promise<void> {
254
+ if (this.preAuthSweepTimer) {
255
+ clearInterval(this.preAuthSweepTimer);
256
+ this.preAuthSweepTimer = null;
257
+ }
258
+ await this.typedServer.stop();
259
+ }
260
+
261
+ public activeSessionCounts(
262
+ bindingArg: plugins.serveZoneInterfaces.data.ICoreMailBindingDesiredState,
263
+ configEpochArg: number,
264
+ ): Array<{ credentialId: string; version: number; count: number }> {
265
+ if (!this.typedServer.smartServe) {
266
+ return [];
267
+ }
268
+ return countCoreMailActiveSessions(
269
+ this.typedServer.smartServe.getWebSocketConnections().map((peerArg) =>
270
+ peerArg.data.get(coreMailSessionKey) as ICoreMailWorkloadSession | undefined
271
+ ),
272
+ bindingArg,
273
+ configEpochArg,
274
+ );
275
+ }
276
+
277
+ public async getReadiness(): Promise<ICoreMailReadiness> {
278
+ const [database, objectStorage, desiredState] = await Promise.all([
279
+ this.database.checkReadiness({ timeoutMs: 1_500 })
280
+ .then((resultArg) => resultArg.ready)
281
+ .catch(() => false),
282
+ this.storage.checkReadiness().catch(() => false),
283
+ this.desiredState.getActiveDesiredState().catch(() => null),
284
+ ]);
285
+ return {
286
+ live: true,
287
+ database,
288
+ objectStorage,
289
+ exactUpload: objectStorage,
290
+ exactPurge: objectStorage,
291
+ controlBootstrap: true,
292
+ gateway: Boolean(desiredState) && this.gateway.isReady(),
293
+ };
294
+ }
295
+
296
+ private registerHandlers(): void {
297
+ this.addHandler<
298
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailAuthenticateControl
299
+ >('coreMailAuthenticateControl', async (requestArg, toolsArg) => {
300
+ const peer = asPeer(toolsArg);
301
+ const session = await this.auth.authenticateControl(peer, requestArg);
302
+ return {
303
+ authenticated: true,
304
+ credentialId: session.credentialId,
305
+ replica: { ...this.config.replica },
306
+ credentialVersion: session.credentialVersion,
307
+ };
308
+ });
309
+ this.addHandler<
310
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailAuthenticateWorkload
311
+ >('coreMailAuthenticateWorkload', async (requestArg, toolsArg) => {
312
+ const session = await this.auth.authenticateWorkload(asPeer(toolsArg), requestArg);
313
+ return {
314
+ authenticated: true,
315
+ credentialId: session.credentialId,
316
+ credentialVersion: session.credentialVersion,
317
+ bindingState: session.bindingState,
318
+ capabilities: [...session.capabilities],
319
+ allowedOperations: [...session.allowedOperations],
320
+ coreMailTransferOrigin: session.coreMailTransferOrigin,
321
+ };
322
+ });
323
+ this.addHandler<
324
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailPrepareDesiredStateUpload
325
+ >('coreMailPrepareDesiredStateUpload', async (requestArg, toolsArg) => {
326
+ this.auth.requireControlSession(asPeer(toolsArg));
327
+ return await this.desiredState.prepare(requestArg);
328
+ });
329
+ this.addHandler<
330
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailApplyDesiredState
331
+ >('coreMailApplyDesiredState', async (requestArg, toolsArg) => {
332
+ this.auth.requireControlSession(asPeer(toolsArg));
333
+ return { status: await this.desiredState.apply(requestArg) };
334
+ });
335
+ this.addHandler<
336
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGetReconciliationStatus
337
+ >('coreMailGetReconciliationStatus', async (requestArg, toolsArg) => {
338
+ this.auth.requireControlSession(asPeer(toolsArg));
339
+ return { status: await this.desiredState.getStatus(requestArg) };
340
+ });
341
+ this.addHandler<
342
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundSubmission
343
+ >('coreMailPrepareOutboundSubmission', async (requestArg, toolsArg) => {
344
+ const { session, binding } = await this.auth.requireWorkloadSession(
345
+ asPeer(toolsArg),
346
+ 'coreMailPrepareOutboundSubmission',
347
+ );
348
+ return await this.submissions.prepare(session, binding, requestArg);
349
+ });
350
+ this.addHandler<
351
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailPrepareOutboundPartUpload
352
+ >('coreMailPrepareOutboundPartUpload', async (requestArg, toolsArg) => {
353
+ const { session } = await this.auth.requireWorkloadSession(
354
+ asPeer(toolsArg),
355
+ 'coreMailPrepareOutboundPartUpload',
356
+ );
357
+ return await this.submissions.preparePartUpload(session, requestArg);
358
+ });
359
+ this.addHandler<
360
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailCompleteOutboundPartUpload
361
+ >('coreMailCompleteOutboundPartUpload', async (requestArg, toolsArg) => {
362
+ const { session } = await this.auth.requireWorkloadSession(
363
+ asPeer(toolsArg),
364
+ 'coreMailCompleteOutboundPartUpload',
365
+ );
366
+ return await this.submissions.completePartUpload(session, requestArg);
367
+ });
368
+ this.addHandler<
369
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailFinalizeOutboundSubmission
370
+ >('coreMailFinalizeOutboundSubmission', async (requestArg, toolsArg) => {
371
+ const { session } = await this.auth.requireWorkloadSession(
372
+ asPeer(toolsArg),
373
+ 'coreMailFinalizeOutboundSubmission',
374
+ );
375
+ return await this.submissions.finalize(session, requestArg);
376
+ });
377
+ this.addHandler<
378
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailGetOutboundSubmission
379
+ >('coreMailGetOutboundSubmission', async (requestArg, toolsArg) => {
380
+ const { session } = await this.auth.requireWorkloadSession(
381
+ asPeer(toolsArg),
382
+ 'coreMailGetOutboundSubmission',
383
+ );
384
+ return await this.submissions.get(session, requestArg);
385
+ });
386
+ this.addHandler<
387
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailListInboundDeliveries
388
+ >('coreMailListInboundDeliveries', async (requestArg, toolsArg) => {
389
+ const { session } = await this.auth.requireWorkloadSession(
390
+ asPeer(toolsArg),
391
+ 'coreMailListInboundDeliveries',
392
+ );
393
+ return await this.inbound.listDeliveries(session, requestArg);
394
+ });
395
+ this.addHandler<
396
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailPrepareInboundFetch
397
+ >('coreMailPrepareInboundFetch', async (requestArg, toolsArg) => {
398
+ const { session } = await this.auth.requireWorkloadSession(
399
+ asPeer(toolsArg),
400
+ 'coreMailPrepareInboundFetch',
401
+ );
402
+ return await this.inbound.prepareFetch(session, requestArg);
403
+ });
404
+ this.addHandler<
405
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailCompleteInboundFetch
406
+ >('coreMailCompleteInboundFetch', async (requestArg, toolsArg) => {
407
+ const { session } = await this.auth.requireWorkloadSession(
408
+ asPeer(toolsArg),
409
+ 'coreMailCompleteInboundFetch',
410
+ );
411
+ return await this.inbound.completeFetch(session, requestArg);
412
+ });
413
+ this.addHandler<
414
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailAcknowledgeInboundDelivery
415
+ >('coreMailAcknowledgeInboundDelivery', async (requestArg, toolsArg) => {
416
+ const { session } = await this.auth.requireWorkloadSession(
417
+ asPeer(toolsArg),
418
+ 'coreMailAcknowledgeInboundDelivery',
419
+ );
420
+ return await this.inbound.acknowledge(session, requestArg);
421
+ });
422
+ }
423
+
424
+ private addHandler<
425
+ TRequest extends plugins.typedrequestInterfaces.ITypedRequest,
426
+ >(
427
+ methodArg: TRequest['method'],
428
+ handlerArg: (
429
+ requestArg: TRequest['request'],
430
+ toolsArg: ICoreMailTypedTools | undefined,
431
+ ) => Promise<TRequest['response']>,
432
+ ): void {
433
+ this.router.addTypedHandler(new plugins.typedrequest.TypedHandler<TRequest>(
434
+ methodArg,
435
+ async (requestArg, toolsArg) => {
436
+ try {
437
+ return await handlerArg(requestArg, toolsArg);
438
+ } catch (error) {
439
+ throw privacySafeError(error);
440
+ }
441
+ },
442
+ ));
443
+ }
444
+ }