@serve.zone/dcrouter 18.4.1 → 18.6.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/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +2 -1
- package/dist_ts/classes.dcrouter.js +4 -2
- package/dist_ts/db/documents/classes.gateway-mail-domain.doc.d.ts +16 -0
- package/dist_ts/db/documents/classes.gateway-mail-domain.doc.js +107 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/email/classes.gateway-mail-domain.store.d.ts +11 -0
- package/dist_ts/email/classes.gateway-mail-domain.store.js +70 -0
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +5 -23
- package/dist_ts/email/classes.workapp-mail-manager.js +63 -35
- package/dist_ts/email/gateway-mail-domain-authority.d.ts +44 -0
- package/dist_ts/email/gateway-mail-domain-authority.js +160 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +33 -1
- package/dist_ts/opsserver/helpers/gateway-client-auth.d.ts +2 -0
- package/dist_ts/opsserver/helpers/gateway-client-auth.js +5 -3
- package/dist_ts_migrations/index.js +196 -11
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +2 -2
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +3 -1
- package/ts/db/documents/classes.gateway-mail-domain.doc.ts +42 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/email/classes.gateway-mail-domain.store.ts +89 -0
- package/ts/email/classes.workapp-mail-manager.ts +90 -68
- package/ts/email/gateway-mail-domain-authority.ts +222 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +43 -1
- package/ts/opsserver/helpers/gateway-client-auth.ts +6 -2
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/readme.plan.md +0 -22
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IWorkAppMailInboundRoute,
|
|
4
|
+
IWorkAppMailOwnership,
|
|
5
|
+
} from '../../ts_interfaces/data/workhoster.js';
|
|
6
|
+
import type { TGatewayClientType } from '../../ts_interfaces/data/route-management.js';
|
|
7
|
+
|
|
8
|
+
export interface IStoredWorkAppMailIdentity {
|
|
9
|
+
id: string;
|
|
10
|
+
externalKey: string;
|
|
11
|
+
ownership: IWorkAppMailOwnership;
|
|
12
|
+
address: string;
|
|
13
|
+
localPart: string;
|
|
14
|
+
domain: string;
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
displayName?: string;
|
|
17
|
+
inbound?: IWorkAppMailInboundRoute;
|
|
18
|
+
inboundTarget?: plugins.servezoneInterfaces.data.IMailInboundTarget;
|
|
19
|
+
smtp: {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
username: string;
|
|
22
|
+
};
|
|
23
|
+
createdAt: number;
|
|
24
|
+
updatedAt: number;
|
|
25
|
+
createdBy: string;
|
|
26
|
+
smtpPassword: string;
|
|
27
|
+
smtpLastRotatedAt?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface IGatewayMailDomainOwner {
|
|
31
|
+
gatewayClientType: TGatewayClientType;
|
|
32
|
+
gatewayClientId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface IGatewayMailDomainGroup extends IGatewayMailDomainOwner {
|
|
36
|
+
id: string;
|
|
37
|
+
domain: string;
|
|
38
|
+
identities: IStoredWorkAppMailIdentity[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const gatewayClientTypes = new Set<TGatewayClientType>(['onebox', 'cloudly', 'custom']);
|
|
42
|
+
|
|
43
|
+
const requireTrimmedString = (valueArg: unknown, fieldNameArg: string): string => {
|
|
44
|
+
const value = typeof valueArg === 'string' ? valueArg.trim() : '';
|
|
45
|
+
if (!value) throw new Error(`${fieldNameArg} is required`);
|
|
46
|
+
return value;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const requireTimestamp = (valueArg: unknown, fieldNameArg: string): number => {
|
|
50
|
+
if (!Number.isSafeInteger(valueArg) || Number(valueArg) < 0) {
|
|
51
|
+
throw new Error(`${fieldNameArg} must be a non-negative safe integer`);
|
|
52
|
+
}
|
|
53
|
+
return Number(valueArg);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function normalizeGatewayMailDomain(domainArg: unknown): string {
|
|
57
|
+
const candidate = typeof domainArg === 'string'
|
|
58
|
+
? domainArg.trim().toLowerCase().replace(/\.+$/, '')
|
|
59
|
+
: '';
|
|
60
|
+
if (!candidate || candidate.includes('@') || !candidate.includes('.')) {
|
|
61
|
+
throw new Error(`Invalid email domain: ${String(domainArg || '')}`);
|
|
62
|
+
}
|
|
63
|
+
const ascii = plugins.url.domainToASCII(candidate);
|
|
64
|
+
if (!ascii || ascii.includes('@') || !ascii.includes('.')) {
|
|
65
|
+
throw new Error(`Invalid email domain: ${String(domainArg || '')}`);
|
|
66
|
+
}
|
|
67
|
+
return ascii;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function normalizeGatewayMailDomainOwner(
|
|
71
|
+
ownerArg: {
|
|
72
|
+
gatewayClientType?: unknown;
|
|
73
|
+
gatewayClientId?: unknown;
|
|
74
|
+
},
|
|
75
|
+
): IGatewayMailDomainOwner {
|
|
76
|
+
const gatewayClientType = ownerArg?.gatewayClientType as TGatewayClientType;
|
|
77
|
+
if (!gatewayClientTypes.has(gatewayClientType)) {
|
|
78
|
+
throw new Error(`Invalid gateway client type: ${String(ownerArg?.gatewayClientType || '')}`);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
gatewayClientType,
|
|
82
|
+
gatewayClientId: requireTrimmedString(ownerArg.gatewayClientId, 'gatewayClientId'),
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function gatewayMailOwnerFromLegacyOwnership(
|
|
87
|
+
ownershipArg: IWorkAppMailOwnership,
|
|
88
|
+
): IGatewayMailDomainOwner & { appInstanceId: string } {
|
|
89
|
+
const owner = normalizeGatewayMailDomainOwner({
|
|
90
|
+
gatewayClientType: ownershipArg?.workHosterType,
|
|
91
|
+
gatewayClientId: ownershipArg?.workHosterId,
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
...owner,
|
|
95
|
+
appInstanceId: requireTrimmedString(ownershipArg?.workAppId, 'appInstanceId'),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function createGatewayMailDomainId(
|
|
100
|
+
ownerArg: IGatewayMailDomainOwner,
|
|
101
|
+
domainArg: string,
|
|
102
|
+
): string {
|
|
103
|
+
const owner = normalizeGatewayMailDomainOwner(ownerArg);
|
|
104
|
+
const domain = normalizeGatewayMailDomain(domainArg);
|
|
105
|
+
const digest = plugins.crypto.createHash('sha256')
|
|
106
|
+
.update(JSON.stringify([owner.gatewayClientType, owner.gatewayClientId, domain]))
|
|
107
|
+
.digest('hex');
|
|
108
|
+
return `gateway-mail-domain-${digest}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function normalizeStoredWorkAppMailIdentity(
|
|
112
|
+
identityArg: unknown,
|
|
113
|
+
): IStoredWorkAppMailIdentity {
|
|
114
|
+
if (!identityArg || typeof identityArg !== 'object' || Array.isArray(identityArg)) {
|
|
115
|
+
throw new Error('Stored WorkApp mail identity must be an object');
|
|
116
|
+
}
|
|
117
|
+
const identity = identityArg as Record<string, any>;
|
|
118
|
+
const owner = gatewayMailOwnerFromLegacyOwnership(identity.ownership);
|
|
119
|
+
const domain = normalizeGatewayMailDomain(identity.domain);
|
|
120
|
+
const localPart = requireTrimmedString(identity.localPart, 'localPart').toLowerCase();
|
|
121
|
+
if (localPart.includes('@') || /\s/.test(localPart)) {
|
|
122
|
+
throw new Error(`Invalid email local part: ${identity.localPart}`);
|
|
123
|
+
}
|
|
124
|
+
const rawAddress = requireTrimmedString(identity.address, 'address').toLowerCase();
|
|
125
|
+
const [addressLocalPart, addressDomain, addressExtra] = rawAddress.split('@');
|
|
126
|
+
if (!addressLocalPart || !addressDomain || addressExtra) {
|
|
127
|
+
throw new Error(`Invalid stored mail identity address: ${identity.address}`);
|
|
128
|
+
}
|
|
129
|
+
const address = `${addressLocalPart}@${normalizeGatewayMailDomain(addressDomain)}`;
|
|
130
|
+
if (address !== `${localPart}@${domain}`) {
|
|
131
|
+
throw new Error('Stored mail identity address, localPart, and domain do not match');
|
|
132
|
+
}
|
|
133
|
+
if (!identity.smtp || typeof identity.smtp !== 'object' || Array.isArray(identity.smtp)) {
|
|
134
|
+
throw new Error('Stored mail identity smtp configuration is required');
|
|
135
|
+
}
|
|
136
|
+
if (typeof identity.enabled !== 'boolean' || typeof identity.smtp.enabled !== 'boolean') {
|
|
137
|
+
throw new Error('Stored mail identity enabled flags must be boolean');
|
|
138
|
+
}
|
|
139
|
+
const createdAt = requireTimestamp(identity.createdAt, 'createdAt');
|
|
140
|
+
const updatedAt = requireTimestamp(identity.updatedAt, 'updatedAt');
|
|
141
|
+
const smtpLastRotatedAt = identity.smtpLastRotatedAt === undefined
|
|
142
|
+
? undefined
|
|
143
|
+
: requireTimestamp(identity.smtpLastRotatedAt, 'smtpLastRotatedAt');
|
|
144
|
+
const ownership: IWorkAppMailOwnership = {
|
|
145
|
+
workHosterType: owner.gatewayClientType,
|
|
146
|
+
workHosterId: owner.gatewayClientId,
|
|
147
|
+
workAppId: owner.appInstanceId,
|
|
148
|
+
};
|
|
149
|
+
const externalKey = [
|
|
150
|
+
owner.gatewayClientType,
|
|
151
|
+
owner.gatewayClientId,
|
|
152
|
+
owner.appInstanceId,
|
|
153
|
+
address,
|
|
154
|
+
].join(':');
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
id: requireTrimmedString(identity.id, 'id'),
|
|
158
|
+
externalKey,
|
|
159
|
+
ownership,
|
|
160
|
+
address,
|
|
161
|
+
localPart,
|
|
162
|
+
domain,
|
|
163
|
+
enabled: identity.enabled,
|
|
164
|
+
...(identity.displayName === undefined
|
|
165
|
+
? {}
|
|
166
|
+
: { displayName: String(identity.displayName) }),
|
|
167
|
+
...(identity.inbound === undefined
|
|
168
|
+
? {}
|
|
169
|
+
: { inbound: structuredClone(identity.inbound) }),
|
|
170
|
+
...(identity.inboundTarget === undefined
|
|
171
|
+
? {}
|
|
172
|
+
: { inboundTarget: structuredClone(identity.inboundTarget) }),
|
|
173
|
+
smtp: {
|
|
174
|
+
enabled: identity.smtp.enabled,
|
|
175
|
+
username: requireTrimmedString(identity.smtp.username, 'smtp.username'),
|
|
176
|
+
},
|
|
177
|
+
createdAt,
|
|
178
|
+
updatedAt,
|
|
179
|
+
createdBy: requireTrimmedString(identity.createdBy, 'createdBy'),
|
|
180
|
+
smtpPassword: requireTrimmedString(identity.smtpPassword, 'smtpPassword'),
|
|
181
|
+
...(smtpLastRotatedAt === undefined ? {} : { smtpLastRotatedAt }),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function groupGatewayMailIdentities(
|
|
186
|
+
identitiesArg: unknown[],
|
|
187
|
+
): IGatewayMailDomainGroup[] {
|
|
188
|
+
const groups = new Map<string, IGatewayMailDomainGroup>();
|
|
189
|
+
const identityIds = new Set<string>();
|
|
190
|
+
const externalKeys = new Set<string>();
|
|
191
|
+
|
|
192
|
+
for (const identityArg of identitiesArg) {
|
|
193
|
+
const identity = normalizeStoredWorkAppMailIdentity(identityArg);
|
|
194
|
+
if (identityIds.has(identity.id)) {
|
|
195
|
+
throw new Error(`Duplicate stored mail identity id: ${identity.id}`);
|
|
196
|
+
}
|
|
197
|
+
if (externalKeys.has(identity.externalKey)) {
|
|
198
|
+
throw new Error(`Duplicate stored mail identity externalKey: ${identity.externalKey}`);
|
|
199
|
+
}
|
|
200
|
+
identityIds.add(identity.id);
|
|
201
|
+
externalKeys.add(identity.externalKey);
|
|
202
|
+
const owner = gatewayMailOwnerFromLegacyOwnership(identity.ownership);
|
|
203
|
+
const id = createGatewayMailDomainId(owner, identity.domain);
|
|
204
|
+
const group = groups.get(id) || {
|
|
205
|
+
id,
|
|
206
|
+
gatewayClientType: owner.gatewayClientType,
|
|
207
|
+
gatewayClientId: owner.gatewayClientId,
|
|
208
|
+
domain: identity.domain,
|
|
209
|
+
identities: [],
|
|
210
|
+
};
|
|
211
|
+
group.identities.push(identity);
|
|
212
|
+
groups.set(id, group);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return Array.from(groups.values())
|
|
216
|
+
.map((groupArg) => ({
|
|
217
|
+
...groupArg,
|
|
218
|
+
identities: groupArg.identities
|
|
219
|
+
.sort((leftArg, rightArg) => leftArg.externalKey.localeCompare(rightArg.externalKey)),
|
|
220
|
+
}))
|
|
221
|
+
.sort((leftArg, rightArg) => leftArg.id.localeCompare(rightArg.id));
|
|
222
|
+
}
|
package/ts/email/index.ts
CHANGED
|
@@ -8,12 +8,14 @@ export * from './reconcile-mail-dns-singleton.js';
|
|
|
8
8
|
export * from './classes.mail-egress-coordinator.js';
|
|
9
9
|
export * from './classes.mail-edge-eligibility.js';
|
|
10
10
|
export * from './classes.email-settings.manager.js';
|
|
11
|
+
export * from './classes.gateway-mail-domain.store.js';
|
|
11
12
|
export * from './classes.smartmta-blob-storage-manager.js';
|
|
12
13
|
export * from './classes.smartmta-storage-manager.js';
|
|
13
14
|
export * from './smartmta-storage-policy.js';
|
|
14
15
|
export * from './classes.smtp-account.manager.js';
|
|
15
16
|
export * from './smtp-scram.js';
|
|
16
17
|
export * from './classes.workapp-mail-manager.js';
|
|
18
|
+
export * from './gateway-mail-domain-authority.js';
|
|
17
19
|
export * from './email-dns-records.js';
|
|
18
20
|
export * from './helpers.email-log-traffic.js';
|
|
19
21
|
export * from './helpers.inbound-security.js';
|
|
@@ -32,7 +32,7 @@ export class GatewayClientHandler {
|
|
|
32
32
|
private async requireAuth(
|
|
33
33
|
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
34
34
|
requiredScope?: interfaces.data.TApiTokenScope,
|
|
35
|
-
options: { allowCandidate?: boolean } = {},
|
|
35
|
+
options: { allowCandidate?: boolean; loadManagedZoneNames?: boolean } = {},
|
|
36
36
|
): Promise<TAuthContext> {
|
|
37
37
|
return await requireGatewayMachineAuth(
|
|
38
38
|
this.opsServerRef,
|
|
@@ -351,6 +351,48 @@ export class GatewayClientHandler {
|
|
|
351
351
|
),
|
|
352
352
|
);
|
|
353
353
|
|
|
354
|
+
this.typedrouter.addTypedHandler(
|
|
355
|
+
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.gateway.IReq_GetGatewayClientMailDomainCount>(
|
|
356
|
+
'getGatewayClientMailDomainCount',
|
|
357
|
+
async (dataArg) => {
|
|
358
|
+
const auth = await this.requireAuth(dataArg, 'gateway-clients:read', {
|
|
359
|
+
loadManagedZoneNames: false,
|
|
360
|
+
});
|
|
361
|
+
this.assertCapability(auth, 'readMail');
|
|
362
|
+
const untypedRequest = dataArg as Record<string, unknown>;
|
|
363
|
+
for (const forbiddenKey of [
|
|
364
|
+
'owner',
|
|
365
|
+
'gatewayClientId',
|
|
366
|
+
'gatewayClientType',
|
|
367
|
+
'workHosterId',
|
|
368
|
+
'workHosterType',
|
|
369
|
+
'appInstanceId',
|
|
370
|
+
]) {
|
|
371
|
+
if (Object.prototype.hasOwnProperty.call(untypedRequest, forbiddenKey)) {
|
|
372
|
+
throw new plugins.typedrequest.TypedResponseError(
|
|
373
|
+
'mail-domain count ownership is derived from the gateway-client credential',
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (
|
|
378
|
+
auth.isAdmin
|
|
379
|
+
|| auth.policy?.role !== 'gatewayClient'
|
|
380
|
+
|| !auth.gatewayClient
|
|
381
|
+
|| !auth.policy.gatewayClient
|
|
382
|
+
) {
|
|
383
|
+
throw new plugins.typedrequest.TypedResponseError('gateway-client credential required');
|
|
384
|
+
}
|
|
385
|
+
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
|
386
|
+
return {
|
|
387
|
+
count: await manager.countMailDomains({
|
|
388
|
+
gatewayClientType: auth.gatewayClient.type,
|
|
389
|
+
gatewayClientId: auth.gatewayClient.id,
|
|
390
|
+
}),
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
),
|
|
394
|
+
);
|
|
395
|
+
|
|
354
396
|
this.typedrouter.addTypedHandler(
|
|
355
397
|
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.gateway.IReq_GetGatewayClientDomains>(
|
|
356
398
|
'getGatewayClientDomains',
|
|
@@ -17,6 +17,8 @@ export type TGatewayCredentialState = 'candidate' | 'active' | 'manual';
|
|
|
17
17
|
|
|
18
18
|
export interface IRequireGatewayMachineAuthOptions {
|
|
19
19
|
allowCandidate?: boolean;
|
|
20
|
+
/** Skip managed DNS-domain loading for APIs that do not evaluate hostnames. */
|
|
21
|
+
loadManagedZoneNames?: boolean;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export function getGatewayCredentialState(
|
|
@@ -124,8 +126,10 @@ export async function requireGatewayMachineAuth(
|
|
|
124
126
|
scope: scopeArg,
|
|
125
127
|
requireAdminIdentity: false,
|
|
126
128
|
});
|
|
127
|
-
const managedZoneNames =
|
|
128
|
-
|
|
129
|
+
const managedZoneNames = optionsArg.loadManagedZoneNames === false
|
|
130
|
+
? []
|
|
131
|
+
: (await opsServerRefArg.dcRouterRef.dnsManager?.listDomains() || [])
|
|
132
|
+
.map((domainArg) => domainArg.name);
|
|
129
133
|
|
|
130
134
|
if (auth.isAdmin) {
|
|
131
135
|
return {
|
package/readme.plan.md
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
# Platform refactoring program
|
|
2
|
-
|
|
3
|
-
Status: approved 2026-07-21, derived from that day's incident ledger (deploy-verification crashes, unbounded scans, DNS/IP logic duplication, version-skew scares). Upstream foundations are RELEASED; consumer adoption and the dcrouter-internal refactors are sequenced below — do not start them mid-wave, fold them into the next natural release of each surface.
|
|
4
|
-
|
|
5
|
-
## Shipped upstream (adopt on next dependency bump)
|
|
6
|
-
|
|
7
|
-
1. **Typed-request error boundary — already existed.** @api.global/typedrequest ≥ 3.3.2 sanitizes every handler/middleware throw into a structured error envelope (no socket destruction). Action: ensure every deployed consumer (cloudly, dcrouter, coreflow, onebox) resolves ≥ 3.3.2 and remove any local try/catch layers that only re-implement it.
|
|
8
|
-
2. **Canonical IP helpers — @push.rocks/smartnetwork 4.9.0.** `isPublicIpAddress`, `toReverseDnsName` (non-throwing), `expandIpv6`, alongside the existing `getIpVersion`/`isValidIpv4`/`isValidIpv6`. Adoption: dcrouter ts/email/classes.mail-dns-sync.ts drops its private `toReverseDnsName`/`expandIpv6` (and the `Invalid IP address:` throw); cloudly's `isPublicDeploymentAddress` (classes.deploymentoperationmanager.ts) delegates to `isPublicIpAddress` (IPv4 semantics are byte-identical; IPv6 is 2000::/3 minus 2001:db8::/32).
|
|
9
|
-
3. **Cursor pagination as the org default — @push.rocks/smartdata 7.4.0.** `SmartDataDbDoc.getPagedInstances({ filter, sortField, sortDirection, uniqueField, limit ≤ 1000, cursor, withTotal })` — seek pagination with a unique tiebreaker, never materializes the full set. Adoption: OpsConfigEventDoc.findFiltered becomes a thin wrapper; every future list surface uses it. Review rule: a list API without a cursor is a defect.
|
|
10
|
-
4. **Binary provenance — @git.zone/tsrust 1.7.0.** Every built binary gets a JSON trailer (project, version, git commit, target, builtAt, tsrust version); `tsrust inspect <binary>` prints it. Adoption: bump tsrust across the Rust-bridge repos (smartdb, smartproxy, smartmta, smartnetwork, coretraffic, remoteingress) on their next releases; deployment verification can then assert binary identity instead of guessing (see the strings-on-x86 pitfall in smartdb readme.hints.md).
|
|
11
|
-
|
|
12
|
-
## Sequenced (do NOT start mid-wave)
|
|
13
|
-
|
|
14
|
-
5. **Ops-handler base class (dcrouter).** 25 handler files carry 11 copies of the requireAuth wrapper and 49 hand-written "manager not initialized" guards. Introduce a base with `registerMethod(name, scope, handlerFn)` doing auth, manager guard, error boundary, and realtime invalidation; migrate handlers opportunistically (new handlers must use it; old ones migrate when touched).
|
|
15
|
-
6. **Composition-root service table (dcrouter).** classes.dcrouter.ts (2,326 lines) has 20 near-identical addService blocks with drift (withRetry presence, error logging). Replace with a declarative registration table; behavior-preserving.
|
|
16
|
-
7. **Mail-detail contract dedup (interfaces + catalog + dcrouter).** IEmailDetail and friends exist twice by convention ("matches @serve.zone/catalog"). Move the contract into @serve.zone/interfaces; catalog and dcrouter consume it. Sequence AFTER the in-flight mail-detail wave (countdown, verdicts, transfer stats) fully lands — one migration wave, no compatibility layer.
|
|
17
|
-
8. **Version-skew hardening (conventions).** (a) Tolerant-reader parsing for persisted operation/evidence/state documents that cross release boundaries; (b) version handshake wherever a live process or socket is reused across upgrades (LocalSmartDb engine reuse — smartdb plan Track 6; gateway-client policyGeneration is the precedent).
|
|
18
|
-
|
|
19
|
-
## Non-goals
|
|
20
|
-
|
|
21
|
-
- No big-bang rewrites: every item lands either additive-upstream or inside a surface's next natural release.
|
|
22
|
-
- No compatibility layers (org rule): consumers move to the canonical helpers outright when their wave touches the code.
|