@serve.zone/interfaces 30.11.0 → 30.13.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.
@@ -20,17 +20,22 @@ import {
20
20
  type ICoreMailEnvelope,
21
21
  type ICoreMailErrorData,
22
22
  type ICoreMailGatewayDesiredState,
23
+ type ICoreMailGatewayMessageDescriptor,
23
24
  type ICoreMailGatewayOutboundStatus,
24
25
  type ICoreMailGatewayPeerDesiredState,
25
26
  type ICoreMailInboundDelivery,
26
27
  type ICoreMailInboundDeliveryPage,
28
+ type ICoreMailInboundMailCounters,
27
29
  type ICoreMailMailbox,
30
+ type ICoreMailOutboundMailCounters,
28
31
  type ICoreMailOutboundMessageDescriptor,
29
32
  type ICoreMailPartStatus,
30
33
  type ICoreMailRecipientResolution,
31
34
  type ICoreMailRuntimeKeyReference,
32
35
  type ICoreMailReconciliationStatus,
33
36
  type ICoreMailReplicaIdentity,
37
+ type ICoreMailServiceMailStatistics,
38
+ type ICoreMailSmtpDesiredState,
34
39
  type ICoreMailSubmission,
35
40
  type ICoreMailTransferGrant,
36
41
  type ICoreMailUploadGrant,
@@ -38,8 +43,10 @@ import {
38
43
  type TCoreMailBindingState,
39
44
  type TCoreMailErrorCode,
40
45
  type TCoreMailSha256,
46
+ type TCoreMailSubmissionSource,
41
47
  type TCoreMailWorkloadOperation,
42
48
  } from './coremail.js';
49
+ import type { IMailConnectionInfo } from './mail.js';
43
50
 
44
51
  export const coreMailCanonicalJsonRules = strictCanonicalJsonRules;
45
52
 
@@ -79,6 +86,7 @@ export const coreMailContractLimits = Object.freeze({
79
86
  maximumContentIdBytes: 256,
80
87
  maximumBearerTokenBytes: 1_024,
81
88
  maximumRecipientResolutionMessageBytes: 512,
89
+ maximumConnectionInfoFieldBytes: 512,
82
90
  } as const);
83
91
 
84
92
  export class CoreMailContractError extends Error {
@@ -533,6 +541,24 @@ const requireCanonicalMailbox = (valueArg: unknown, pathArg: string): string =>
533
541
  return value;
534
542
  };
535
543
 
544
+ const requireCanonicalHostname = (valueArg: unknown, pathArg: string): string => {
545
+ const value = requireString(
546
+ valueArg,
547
+ pathArg,
548
+ coreMailContractLimits.maximumMailboxBytes,
549
+ );
550
+ const labels = value.split('.');
551
+ if (
552
+ value !== value.toLowerCase()
553
+ || value.length > 253
554
+ || labels.length < 2
555
+ || labels.some((labelArg) => !mailboxDomainLabelRegex.test(labelArg))
556
+ ) {
557
+ fail(`${pathArg} must be a canonical lowercase hostname`);
558
+ }
559
+ return value;
560
+ };
561
+
536
562
  const normalizeMailbox = (valueArg: unknown, pathArg: string): ICoreMailMailbox => {
537
563
  const record = readRecord(valueArg, pathArg);
538
564
  assertKeys(record, pathArg, ['address'], ['displayName']);
@@ -1157,6 +1183,7 @@ export const normalizeCoreMailSubmission = (
1157
1183
  ],
1158
1184
  [
1159
1185
  'transportMessageId',
1186
+ 'source',
1160
1187
  'nextAttemptAt',
1161
1188
  'error',
1162
1189
  'acceptedAt',
@@ -1209,6 +1236,12 @@ export const normalizeCoreMailSubmission = (
1209
1236
  `${pathArg}.transportMessageId`,
1210
1237
  );
1211
1238
  }
1239
+ if (Object.hasOwn(record, 'source')) {
1240
+ if (record.source !== 'api' && record.source !== 'smtp') {
1241
+ fail(`${pathArg}.source must be api or smtp`);
1242
+ }
1243
+ normalized.source = record.source as TCoreMailSubmissionSource;
1244
+ }
1212
1245
  if (Object.hasOwn(record, 'error')) {
1213
1246
  normalized.error = normalizeCoreMailErrorData(record.error, `${pathArg}.error`);
1214
1247
  }
@@ -1698,6 +1731,38 @@ const normalizeGateway = (
1698
1731
  };
1699
1732
  };
1700
1733
 
1734
+ const normalizeSmtpDesiredState = (
1735
+ valueArg: unknown,
1736
+ pathArg: string,
1737
+ ): ICoreMailSmtpDesiredState => {
1738
+ const record = readRecord(valueArg, pathArg);
1739
+ assertKeys(record, pathArg, ['enabled', 'port', 'hostname', 'tls']);
1740
+ if (typeof record.enabled !== 'boolean') {
1741
+ fail(`${pathArg}.enabled must be boolean`);
1742
+ }
1743
+ const tlsRecord = readRecord(record.tls, `${pathArg}.tls`);
1744
+ assertKeys(
1745
+ tlsRecord,
1746
+ `${pathArg}.tls`,
1747
+ ['certificatePemSecretKey', 'privateKeyPemSecretKey'],
1748
+ );
1749
+ return {
1750
+ enabled: record.enabled as boolean,
1751
+ port: requireSafeInteger(record.port, `${pathArg}.port`, 1, 65_535),
1752
+ hostname: requireCanonicalHostname(record.hostname, `${pathArg}.hostname`),
1753
+ tls: {
1754
+ certificatePemSecretKey: requireEnvironmentKey(
1755
+ tlsRecord.certificatePemSecretKey,
1756
+ `${pathArg}.tls.certificatePemSecretKey`,
1757
+ ),
1758
+ privateKeyPemSecretKey: requireEnvironmentKey(
1759
+ tlsRecord.privateKeyPemSecretKey,
1760
+ `${pathArg}.tls.privateKeyPemSecretKey`,
1761
+ ),
1762
+ },
1763
+ };
1764
+ };
1765
+
1701
1766
  export const normalizeCoreMailDesiredState = (
1702
1767
  valueArg: unknown,
1703
1768
  ): ICoreMailDesiredState => {
@@ -1706,6 +1771,7 @@ export const normalizeCoreMailDesiredState = (
1706
1771
  record,
1707
1772
  'desiredState',
1708
1773
  ['schemaVersion', 'configEpoch', 'bindings', 'gateway', 'cursorKeys'],
1774
+ ['smtp'],
1709
1775
  );
1710
1776
  if (record.schemaVersion !== 2) {
1711
1777
  fail('desiredState.schemaVersion must be 2');
@@ -1736,13 +1802,17 @@ export const normalizeCoreMailDesiredState = (
1736
1802
  bindings.sort((leftArg, rightArg) =>
1737
1803
  compareStrings(leftArg.bindingId, rightArg.bindingId),
1738
1804
  );
1739
- return deepFreezeValue({
1805
+ const normalized: ICoreMailDesiredState = {
1740
1806
  schemaVersion: 2,
1741
1807
  configEpoch: requireSafeInteger(record.configEpoch, 'desiredState.configEpoch', 1),
1742
1808
  bindings,
1743
1809
  gateway: normalizeGateway(record.gateway, 'desiredState.gateway'),
1744
1810
  cursorKeys: normalizeRuntimeKeySet(record.cursorKeys, 'desiredState.cursorKeys'),
1745
- });
1811
+ };
1812
+ if (Object.hasOwn(record, 'smtp')) {
1813
+ normalized.smtp = normalizeSmtpDesiredState(record.smtp, 'desiredState.smtp');
1814
+ }
1815
+ return deepFreezeValue(normalized);
1746
1816
  };
1747
1817
 
1748
1818
  export const normalizeCoreMailReplicaIdentity = (
@@ -1976,3 +2046,209 @@ export const normalizeCoreMailGatewayPeerDesiredState = (
1976
2046
  ),
1977
2047
  });
1978
2048
  };
2049
+
2050
+ const utcCalendarDayRegex = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
2051
+
2052
+ const requireUtcCalendarDay = (valueArg: unknown, pathArg: string): string => {
2053
+ const value = requireString(valueArg, pathArg, 10);
2054
+ if (!utcCalendarDayRegex.test(value)) {
2055
+ fail(`${pathArg} must be a canonical YYYY-MM-DD UTC day`);
2056
+ }
2057
+ const [year, month, day] = value.split('-').map(Number);
2058
+ const parsed = new Date(Date.UTC(year, month - 1, day));
2059
+ if (
2060
+ parsed.getUTCFullYear() !== year
2061
+ || parsed.getUTCMonth() + 1 !== month
2062
+ || parsed.getUTCDate() !== day
2063
+ ) {
2064
+ fail(`${pathArg} must be a real UTC calendar day`);
2065
+ }
2066
+ return value;
2067
+ };
2068
+
2069
+ const normalizeOutboundMailCounters = (
2070
+ valueArg: unknown,
2071
+ pathArg: string,
2072
+ ): ICoreMailOutboundMailCounters => {
2073
+ const record = readRecord(valueArg, pathArg);
2074
+ const keys = [
2075
+ 'submittedApi',
2076
+ 'submittedSmtp',
2077
+ 'delivered',
2078
+ 'deferred',
2079
+ 'failed',
2080
+ 'deadLettered',
2081
+ ] as const;
2082
+ assertKeys(record, pathArg, [...keys]);
2083
+ const normalized = {} as ICoreMailOutboundMailCounters;
2084
+ for (const key of keys) {
2085
+ normalized[key] = requireSafeInteger(record[key], `${pathArg}.${key}`, 0);
2086
+ }
2087
+ return normalized;
2088
+ };
2089
+
2090
+ const normalizeInboundMailCounters = (
2091
+ valueArg: unknown,
2092
+ pathArg: string,
2093
+ ): ICoreMailInboundMailCounters => {
2094
+ const record = readRecord(valueArg, pathArg);
2095
+ const keys = [
2096
+ 'received',
2097
+ 'acknowledgedProcessed',
2098
+ 'acknowledgedDiscarded',
2099
+ ] as const;
2100
+ assertKeys(record, pathArg, [...keys]);
2101
+ const normalized = {} as ICoreMailInboundMailCounters;
2102
+ for (const key of keys) {
2103
+ normalized[key] = requireSafeInteger(record[key], `${pathArg}.${key}`, 0);
2104
+ }
2105
+ return normalized;
2106
+ };
2107
+
2108
+ export const normalizeCoreMailServiceMailStatistics = (
2109
+ valueArg: unknown,
2110
+ pathArg = 'serviceMailStatistics',
2111
+ ): ICoreMailServiceMailStatistics => {
2112
+ const record = readRecord(valueArg, pathArg);
2113
+ assertKeys(
2114
+ record,
2115
+ pathArg,
2116
+ [
2117
+ 'tenantId',
2118
+ 'serviceId',
2119
+ 'bindingId',
2120
+ 'dayUtc',
2121
+ 'outbound',
2122
+ 'inbound',
2123
+ 'updatedAt',
2124
+ ],
2125
+ );
2126
+ return deepFreezeValue({
2127
+ tenantId: requireIdentifier(record.tenantId, `${pathArg}.tenantId`),
2128
+ serviceId: requireIdentifier(record.serviceId, `${pathArg}.serviceId`),
2129
+ bindingId: requireIdentifier(record.bindingId, `${pathArg}.bindingId`),
2130
+ dayUtc: requireUtcCalendarDay(record.dayUtc, `${pathArg}.dayUtc`),
2131
+ outbound: normalizeOutboundMailCounters(record.outbound, `${pathArg}.outbound`),
2132
+ inbound: normalizeInboundMailCounters(record.inbound, `${pathArg}.inbound`),
2133
+ updatedAt: requireSafeInteger(record.updatedAt, `${pathArg}.updatedAt`, 1),
2134
+ });
2135
+ };
2136
+
2137
+ /**
2138
+ * Strict gateway message descriptor normalization.
2139
+ *
2140
+ * CoreMail digests the normalized envelope together with the normalized
2141
+ * connection source, so the emitted field set and order is part of the
2142
+ * contract and must not change.
2143
+ */
2144
+ export const normalizeCoreMailGatewayMessage = (
2145
+ valueArg: unknown,
2146
+ pathArg = 'gatewayMessage',
2147
+ ): ICoreMailGatewayMessageDescriptor => {
2148
+ const record = readRecord(valueArg, pathArg);
2149
+ assertKeys(record, pathArg, ['envelope', 'rawMime']);
2150
+ const rawMime = readRecord(record.rawMime, `${pathArg}.rawMime`);
2151
+ assertKeys(rawMime, `${pathArg}.rawMime`, ['sha256', 'lengthBytes', 'contentType']);
2152
+ if (rawMime.contentType !== 'message/rfc822') {
2153
+ fail(`${pathArg}.rawMime.contentType must be message/rfc822`);
2154
+ }
2155
+ const normalized: ICoreMailGatewayMessageDescriptor = {
2156
+ envelope: normalizeCoreMailEnvelope(record.envelope, `${pathArg}.envelope`),
2157
+ rawMime: {
2158
+ sha256: normalizeCoreMailSha256(rawMime.sha256, `${pathArg}.rawMime.sha256`),
2159
+ lengthBytes: requireSafeInteger(
2160
+ rawMime.lengthBytes,
2161
+ `${pathArg}.rawMime.lengthBytes`,
2162
+ 1,
2163
+ coreMailLimits.serializedMimeBytes,
2164
+ ),
2165
+ contentType: 'message/rfc822',
2166
+ },
2167
+ };
2168
+ return deepFreezeValue(normalized);
2169
+ };
2170
+
2171
+ const requireConnectionInfoField = (valueArg: unknown, pathArg: string): string => {
2172
+ const value = requireString(
2173
+ valueArg,
2174
+ pathArg,
2175
+ coreMailContractLimits.maximumConnectionInfoFieldBytes,
2176
+ );
2177
+ if (value.trim() !== value) {
2178
+ fail(`${pathArg} must not carry leading or trailing whitespace`);
2179
+ }
2180
+ return value;
2181
+ };
2182
+
2183
+ /**
2184
+ * Strict connection-source normalization for gateway resolve and handoff
2185
+ * requests. CoreMail binds routing handles to a digest of the normalized
2186
+ * envelope and this normalized source, so the emitted field set and order is
2187
+ * part of the contract and must not change.
2188
+ */
2189
+ export const normalizeCoreMailConnectionInfo = (
2190
+ valueArg: unknown,
2191
+ pathArg = 'connectionInfo',
2192
+ ): IMailConnectionInfo => {
2193
+ const record = readRecord(valueArg, pathArg);
2194
+ assertKeys(
2195
+ record,
2196
+ pathArg,
2197
+ ['protocol'],
2198
+ [
2199
+ 'remoteAddress',
2200
+ 'proxyAddress',
2201
+ 'heloName',
2202
+ 'tls',
2203
+ 'authUsername',
2204
+ 'authenticatedCredentialId',
2205
+ ],
2206
+ );
2207
+ if (
2208
+ record.protocol !== 'smtp'
2209
+ && record.protocol !== 'submission'
2210
+ && record.protocol !== 'api'
2211
+ ) {
2212
+ fail(`${pathArg}.protocol must be smtp, submission, or api`);
2213
+ }
2214
+ const normalized: IMailConnectionInfo = {
2215
+ protocol: record.protocol as IMailConnectionInfo['protocol'],
2216
+ };
2217
+ if (record.remoteAddress !== undefined) {
2218
+ normalized.remoteAddress = requireConnectionInfoField(
2219
+ record.remoteAddress,
2220
+ `${pathArg}.remoteAddress`,
2221
+ );
2222
+ }
2223
+ if (record.proxyAddress !== undefined) {
2224
+ normalized.proxyAddress = requireConnectionInfoField(
2225
+ record.proxyAddress,
2226
+ `${pathArg}.proxyAddress`,
2227
+ );
2228
+ }
2229
+ if (record.heloName !== undefined) {
2230
+ normalized.heloName = requireConnectionInfoField(
2231
+ record.heloName,
2232
+ `${pathArg}.heloName`,
2233
+ );
2234
+ }
2235
+ if (record.tls !== undefined) {
2236
+ if (typeof record.tls !== 'boolean') {
2237
+ fail(`${pathArg}.tls must be boolean`);
2238
+ }
2239
+ normalized.tls = record.tls as boolean;
2240
+ }
2241
+ if (record.authUsername !== undefined) {
2242
+ normalized.authUsername = requireConnectionInfoField(
2243
+ record.authUsername,
2244
+ `${pathArg}.authUsername`,
2245
+ );
2246
+ }
2247
+ if (record.authenticatedCredentialId !== undefined) {
2248
+ normalized.authenticatedCredentialId = requireConnectionInfoField(
2249
+ record.authenticatedCredentialId,
2250
+ `${pathArg}.authenticatedCredentialId`,
2251
+ );
2252
+ }
2253
+ return deepFreezeValue(normalized);
2254
+ };
@@ -26,6 +26,11 @@ export type TCoreMailSubmissionState =
26
26
  | 'deferred'
27
27
  | 'failed'
28
28
  | 'deadLettered';
29
+ /**
30
+ * Entry point that produced an outbound submission. `api` is the authenticated
31
+ * workload TypedRequest surface, `smtp` is the CoreMail submission listener.
32
+ */
33
+ export type TCoreMailSubmissionSource = 'api' | 'smtp';
29
34
  export type TCoreMailInboundDeliveryState = 'pending' | 'fetching' | 'fetched' | 'acknowledged';
30
35
  export type TCoreMailInboundAckOutcome = 'processed' | 'discarded';
31
36
  export type TCoreMailRecipientAction = 'accept' | 'reject' | 'defer' | 'unhandled';
@@ -149,6 +154,8 @@ export interface ICoreMailSubmission {
149
154
  idempotencyKey: string;
150
155
  submissionDigest: TCoreMailSha256;
151
156
  state: TCoreMailSubmissionState;
157
+ /** Entry point that produced this submission. Omitted means an API submission. */
158
+ source?: TCoreMailSubmissionSource;
152
159
  parts: ICoreMailPartStatus[];
153
160
  transportMessageId?: string;
154
161
  attempts: number;
@@ -255,11 +262,38 @@ export interface ICoreMailRuntimeKeyReference {
255
262
  acceptUntil?: number;
256
263
  }
257
264
 
265
+ export interface ICoreMailSmtpTlsDesiredState {
266
+ /** Keys in CoreMail's resolved runtime secret material holding PEM text, never plaintext values. */
267
+ certificatePemSecretKey: string;
268
+ privateKeyPemSecretKey: string;
269
+ }
270
+
271
+ /**
272
+ * Desired state for CoreMail's own SMTP submission listener (MSA).
273
+ *
274
+ * SMTP AUTH uses the binding identity directly: the username is the
275
+ * `bindingId` and the password is any current or retiring credential secret of
276
+ * that binding, so credential rotation windows behave exactly as they do for
277
+ * the workload TypedRequest surface. The listener stays unavailable until the
278
+ * referenced PEM material resolves; an enabled listener without resolvable
279
+ * certificate and private key material never accepts submissions.
280
+ */
281
+ export interface ICoreMailSmtpDesiredState {
282
+ enabled: boolean;
283
+ /** Listener port inside the CoreMail workload. */
284
+ port: number;
285
+ /** Advertised EHLO hostname. */
286
+ hostname: string;
287
+ tls: ICoreMailSmtpTlsDesiredState;
288
+ }
289
+
258
290
  export interface ICoreMailDesiredState {
259
291
  schemaVersion: 2;
260
292
  configEpoch: number;
261
293
  bindings: ICoreMailBindingDesiredState[];
262
294
  gateway: ICoreMailGatewayDesiredState;
295
+ /** Optional SMTP submission listener. An omitted listener keeps the API-only surface. */
296
+ smtp?: ICoreMailSmtpDesiredState;
263
297
  cursorKeys: ICoreMailRuntimeKeyReference[];
264
298
  }
265
299
 
@@ -293,6 +327,36 @@ export interface ICoreMailReconciliationStatus {
293
327
  errorCode?: TCoreMailErrorCode;
294
328
  }
295
329
 
330
+ export interface ICoreMailOutboundMailCounters {
331
+ submittedApi: number;
332
+ submittedSmtp: number;
333
+ delivered: number;
334
+ deferred: number;
335
+ failed: number;
336
+ deadLettered: number;
337
+ }
338
+
339
+ export interface ICoreMailInboundMailCounters {
340
+ received: number;
341
+ acknowledgedProcessed: number;
342
+ acknowledgedDiscarded: number;
343
+ }
344
+
345
+ /**
346
+ * Per-service mail counters for one UTC calendar day, reported over the
347
+ * authenticated CoreMail control session.
348
+ */
349
+ export interface ICoreMailServiceMailStatistics {
350
+ tenantId: string;
351
+ serviceId: string;
352
+ bindingId: string;
353
+ /** UTC calendar day, `YYYY-MM-DD`. */
354
+ dayUtc: string;
355
+ outbound: ICoreMailOutboundMailCounters;
356
+ inbound: ICoreMailInboundMailCounters;
357
+ updatedAt: number;
358
+ }
359
+
296
360
  export interface ICoreMailRecipientResolution {
297
361
  recipient: string;
298
362
  action: TCoreMailRecipientAction;
@@ -311,6 +375,14 @@ export interface ICoreMailGatewayMessageDescriptor {
311
375
  };
312
376
  }
313
377
 
378
+ /**
379
+ * dcrouter-reported delivery status for one accepted transport message.
380
+ *
381
+ * `updatedAt` must be monotonic per `transportMessageId`. A
382
+ * `transportMessageId` the gateway can never recognize again must be reported
383
+ * as a terminal `failed` status instead of an error, because CoreMail retries
384
+ * errors indefinitely.
385
+ */
314
386
  export interface ICoreMailGatewayOutboundStatus {
315
387
  transportMessageId: string;
316
388
  state: Extract<
package/ts/data/index.ts CHANGED
@@ -23,6 +23,7 @@ export * from './mail.js';
23
23
  export * from './nodecredential.js';
24
24
  export * from './nodeenrollment.js';
25
25
  export * from './networkauthority.js';
26
+ export * from './networkcredential.js';
26
27
  export * from './networkdelivery.js';
27
28
  export * from './networkprojection.js';
28
29
  export type { IRuntimeNetworkRouterPacketGrantV1, IRuntimeNetworkRouterProjectionV1 } from './networkrouter.js';
package/ts/data/mail.ts CHANGED
@@ -2,7 +2,13 @@ export type TMailGatewayClientType = 'onebox' | 'cloudly' | 'custom' | (string &
2
2
 
3
3
  export type TMailAddressPatternType = 'exactAddress' | 'domainWildcard' | 'localPartPattern';
4
4
 
5
- export type TMailInboundTargetType = 'webhook' | 'smtpForward' | 'typedEndpoint' | 'discard' | 'reject';
5
+ export type TMailInboundTargetType =
6
+ | 'webhook'
7
+ | 'smtpForward'
8
+ | 'typedEndpoint'
9
+ | 'coreMail'
10
+ | 'discard'
11
+ | 'reject';
6
12
 
7
13
  export type TMailRecipientPolicyMode = 'acceptAll' | 'staticList' | 'cachedResolver' | 'appResolver';
8
14
 
@@ -50,9 +56,26 @@ export type TMailDeliveryJournalEventType =
50
56
  | 'bounced'
51
57
  | 'complaint';
52
58
 
59
+ /**
60
+ * Error codes a mail submission may return. The CoreMail-fronted path maps the
61
+ * matching `TCoreMailErrorCode` values through unchanged so applications can
62
+ * branch on them regardless of which transport carried the submission.
63
+ */
53
64
  export type TMailSubmissionErrorCode =
54
65
  | 'INVALID_REPLY_TO'
55
- | 'REPLY_TO_FIELD_HEADER_CONFLICT';
66
+ | 'REPLY_TO_FIELD_HEADER_CONFLICT'
67
+ | 'AUTHENTICATION_FAILED'
68
+ | 'CAPABILITY_DENIED'
69
+ | 'INVALID_REQUEST'
70
+ | 'INVALID_MAILBOX'
71
+ | 'INVALID_SENDER'
72
+ | 'INVALID_RECIPIENT'
73
+ | 'INVALID_HEADER'
74
+ | 'PAYLOAD_LIMIT_EXCEEDED'
75
+ | 'QUOTA_EXCEEDED'
76
+ | 'IDEMPOTENCY_CONFLICT'
77
+ | 'STATE_CONFLICT'
78
+ | 'NOT_FOUND';
56
79
 
57
80
  /**
58
81
  * Runtime owner of a mail resource. `appInstanceId` is the deployed WorkApp or service
@@ -94,11 +117,17 @@ export interface IMailTypedEndpointTarget {
94
117
  timeoutMs?: number;
95
118
  }
96
119
 
120
+ export interface IMailCoreMailTarget {
121
+ /** CoreMail service identity that owns the inbound mailbox. */
122
+ coreMailServiceId: string;
123
+ }
124
+
97
125
  export interface IMailInboundTarget {
98
126
  type: TMailInboundTargetType;
99
127
  webhook?: IMailWebhookTarget;
100
128
  smtpForward?: IMailSmtpForwardTarget;
101
129
  typedEndpoint?: IMailTypedEndpointTarget;
130
+ coreMail?: IMailCoreMailTarget;
102
131
  reject?: {
103
132
  smtpCode: number;
104
133
  message: string;
@@ -231,8 +260,11 @@ export type TServiceMailSubmissionTlsMode = 'plain' | 'starttls' | 'implicitTls'
231
260
 
232
261
  export interface IServiceMailInboundConfig {
233
262
  enabled?: boolean;
234
- /** Platformclient apps use typedEndpoint; third-party apps with their own SMTP server use smtpForward. */
235
- targetType?: 'typedEndpoint' | 'smtpForward';
263
+ /**
264
+ * Platformclient apps use typedEndpoint; third-party apps with their own SMTP
265
+ * server use smtpForward; services bound to a CoreMail workload use coreMail.
266
+ */
267
+ targetType?: 'typedEndpoint' | 'smtpForward' | 'coreMail';
236
268
  /** TypedRequest method exposed by platformclient over TypedSocket for inbound delivery. */
237
269
  typedMethod?: string;
238
270
  /** Published Docker/Swarm port that dcrouter may SMTP-forward inbound mail to. */
@@ -0,0 +1,88 @@
1
+ import type { IRuntimeAuthorityReferenceV1 } from './runtimeassignment.js';
2
+ import { snapshotRuntimeSessionBindingV1, type IRuntimeSessionBindingV1 } from './runtimesession.js';
3
+ import { validateRuntimeNetworkProjectionV1, type IRuntimeNetworkProjectionV1 } from './networkprojection.js';
4
+ import { captureNetwork, failNetwork, networkCounter, networkEqual, networkId, networkIpv4,
5
+ networkRecord, networkReference, networkReferenceOf, networkSafe, requireNetwork } from '../private/networkcontract.js';
6
+
7
+ export interface IGetRuntimeManagedVpnCredentialRequestV1 {
8
+ session: IRuntimeSessionBindingV1;
9
+ projection: IRuntimeAuthorityReferenceV1;
10
+ }
11
+
12
+ /** Secret response over the current authenticated physical peer only. Never
13
+ * persist, hash, log, or include it in a signed network projection. The native
14
+ * Noise owner validates and uses the keys; these shapes confer no TUN readiness. */
15
+ export interface IGetRuntimeManagedVpnCredentialResponseV1 extends IGetRuntimeManagedVpnCredentialRequestV1 {
16
+ transport: 'quic';
17
+ relay: { id: string; address: string; port: number };
18
+ authorityId: string;
19
+ serverPublicKey: string;
20
+ clientPublicKey: string;
21
+ clientPrivateKey: string;
22
+ /** Unix milliseconds; cannot outlive the referenced projection's DNS lease. */
23
+ expiresAt: number;
24
+ }
25
+
26
+ /** Canonical 32-byte key encoding only. Cryptographic key agreement and pair
27
+ * validation belong to the native Noise implementation, not this JSON parser. */
28
+ const key = (value: unknown): string => {
29
+ requireNetwork(typeof value === 'string' && /^[A-Za-z0-9+/]{43}=$/.test(value));
30
+ const decoded = atob(value);
31
+ requireNetwork(decoded.length === 32 && btoa(decoded) === value && [...decoded].some(byte => byte.charCodeAt(0) !== 0));
32
+ return value;
33
+ };
34
+
35
+ export const snapshotGetRuntimeManagedVpnCredentialRequestV1 = (value: unknown): IGetRuntimeManagedVpnCredentialRequestV1 => networkSafe(() => {
36
+ const item = networkRecord(captureNetwork(value), ['session', 'projection']);
37
+ return { session: snapshotRuntimeSessionBindingV1(item.session), projection: networkReference(item.projection) };
38
+ });
39
+
40
+ export const snapshotGetRuntimeManagedVpnCredentialResponseV1 = (value: unknown): IGetRuntimeManagedVpnCredentialResponseV1 => networkSafe(() => {
41
+ const item = networkRecord(captureNetwork(value), ['session', 'projection', 'transport', 'relay', 'authorityId',
42
+ 'serverPublicKey', 'clientPublicKey', 'clientPrivateKey', 'expiresAt']);
43
+ const relay = networkRecord(item.relay, ['id', 'address', 'port']); requireNetwork(item.transport === 'quic');
44
+ return { session: snapshotRuntimeSessionBindingV1(item.session), projection: networkReference(item.projection), transport: 'quic',
45
+ relay: { id: networkId(relay.id), address: networkIpv4(relay.address), port: networkCounter(relay.port, 65535) },
46
+ authorityId: networkId(item.authorityId), serverPublicKey: key(item.serverPublicKey), clientPublicKey: key(item.clientPublicKey),
47
+ clientPrivateKey: key(item.clientPrivateKey), expiresAt: networkCounter(item.expiresAt) };
48
+ });
49
+
50
+ const bindRequest = (request: IGetRuntimeManagedVpnCredentialRequestV1,
51
+ projection: IRuntimeNetworkProjectionV1, trusted: IRuntimeSessionBindingV1): void => {
52
+ requireNetwork(networkEqual(request.session, trusted) && networkEqual(request.projection, networkReferenceOf(projection))
53
+ && networkEqual(projection.controller, trusted.controller) && projection.nodeId === trusted.nodeId
54
+ && projection.runtimeNamespace === trusted.runtimeNamespace && projection.handoff);
55
+ };
56
+
57
+ /** Content binding only. The caller must resolve the exact current projection
58
+ * and authenticate/fence the current physical peer before and after native work. */
59
+ export const bindGetRuntimeManagedVpnCredentialRequestV1 = async (value: unknown, projectionValue: unknown,
60
+ trustedSessionValue: unknown): Promise<IGetRuntimeManagedVpnCredentialRequestV1> => {
61
+ try {
62
+ const request = snapshotGetRuntimeManagedVpnCredentialRequestV1(value);
63
+ const trusted = snapshotRuntimeSessionBindingV1(trustedSessionValue);
64
+ const projection = await validateRuntimeNetworkProjectionV1(projectionValue);
65
+ bindRequest(request, projection, trusted);
66
+ return request;
67
+ } catch { return failNetwork(); }
68
+ };
69
+
70
+ /** Bind the secret response to the original request and exact selected router
71
+ * UDP endpoint. Time is caller-qualified; this helper does not sample a clock,
72
+ * authenticate a transport, admit policy or prove a running managed connection. */
73
+ export const bindGetRuntimeManagedVpnCredentialResponseV1 = async (value: unknown, requestValue: unknown,
74
+ projectionValue: unknown, trustedSessionValue: unknown, nowValue: unknown): Promise<IGetRuntimeManagedVpnCredentialResponseV1> => {
75
+ try {
76
+ const response = snapshotGetRuntimeManagedVpnCredentialResponseV1(value);
77
+ const request = snapshotGetRuntimeManagedVpnCredentialRequestV1(requestValue);
78
+ const trusted = snapshotRuntimeSessionBindingV1(trustedSessionValue), now = networkCounter(nowValue);
79
+ const projection = await validateRuntimeNetworkProjectionV1(projectionValue);
80
+ bindRequest(request, projection, trusted);
81
+ requireNetwork(networkEqual(response.session, request.session) && networkEqual(response.projection, request.projection));
82
+ requireNetwork(projection.router?.platformEndpointIds.includes(response.relay.id));
83
+ const endpoint = projection.protectedAuthority.platformEndpoints.find(item => item.id === response.relay.id);
84
+ requireNetwork(endpoint && endpoint.protocol === 'udp' && endpoint.address === response.relay.address && endpoint.port === response.relay.port);
85
+ requireNetwork(projection.dns.notBefore <= now && now < response.expiresAt && response.expiresAt <= projection.dns.expiresAt);
86
+ return response;
87
+ } catch { return failNetwork(); }
88
+ };