Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a381df937 | |||
| 38e2f3cee1 | |||
| 4a47460bf1 | |||
| 3679cba3a4 |
+12
-8
@@ -3,19 +3,23 @@
|
||||
## Pending
|
||||
|
||||
|
||||
## 2026-06-01 - 13.41.2
|
||||
|
||||
### Fixes
|
||||
|
||||
- update SmartProxy and RemoteIngress dependencies (deps)
|
||||
- Bump SmartProxy to 27.12.3 for the published half-close regression coverage.
|
||||
- Bump RemoteIngress to 4.22.4 for the half-close/reset and UDP startup lifecycle fixes.
|
||||
- Align npm and Deno import metadata for both runtime dependencies.
|
||||
|
||||
## 2026-05-31 - 13.41.1
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- prevent SmartAcme startup from blocking router startup (smartacme)
|
||||
- Start SmartAcme in the background with bounded exponential retry handling
|
||||
- Re-trigger certificate provisioning after SmartAcme becomes ready
|
||||
- Cancel stale retry timers and clean up SmartAcme instances during shutdown or config updates
|
||||
|
||||
## 2026-05-31 - 13.41.0
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"version": "13.41.0",
|
||||
"version": "13.41.2",
|
||||
"exports": "./binary/dcrouter.ts",
|
||||
"compile": {
|
||||
"include": [
|
||||
@@ -31,7 +31,7 @@
|
||||
"@push.rocks/smartnetwork": "npm:@push.rocks/smartnetwork@^4.7.2",
|
||||
"@push.rocks/smartpath": "npm:@push.rocks/smartpath@^6.0.0",
|
||||
"@push.rocks/smartpromise": "npm:@push.rocks/smartpromise@^4.2.4",
|
||||
"@push.rocks/smartproxy": "npm:@push.rocks/smartproxy@^27.12.2",
|
||||
"@push.rocks/smartproxy": "npm:@push.rocks/smartproxy@^27.12.3",
|
||||
"@push.rocks/smartradius": "npm:@push.rocks/smartradius@^1.1.2",
|
||||
"@push.rocks/smartrequest": "npm:@push.rocks/smartrequest@^5.0.3",
|
||||
"@push.rocks/smartrx": "npm:@push.rocks/smartrx@^3.0.10",
|
||||
@@ -40,7 +40,7 @@
|
||||
"@push.rocks/smartvpn": "npm:@push.rocks/smartvpn@1.20.0",
|
||||
"@push.rocks/taskbuffer": "npm:@push.rocks/taskbuffer@^8.0.2",
|
||||
"@serve.zone/interfaces": "npm:@serve.zone/interfaces@^5.8.0",
|
||||
"@serve.zone/remoteingress": "npm:@serve.zone/remoteingress@^4.22.3",
|
||||
"@serve.zone/remoteingress": "npm:@serve.zone/remoteingress@^4.22.4",
|
||||
"@tsclass/tsclass": "npm:@tsclass/tsclass@^9.5.1",
|
||||
"lru-cache": "npm:lru-cache@^11.4.0",
|
||||
"qrcode": "npm:qrcode@^1.5.4",
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/dcrouter",
|
||||
"private": false,
|
||||
"version": "13.41.0",
|
||||
"version": "13.41.2",
|
||||
"description": "A multifaceted routing service handling mail and SMS delivery functions.",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
@@ -61,7 +61,7 @@
|
||||
"@push.rocks/smartnetwork": "^4.7.2",
|
||||
"@push.rocks/smartpath": "^6.0.0",
|
||||
"@push.rocks/smartpromise": "^4.2.4",
|
||||
"@push.rocks/smartproxy": "^27.12.2",
|
||||
"@push.rocks/smartproxy": "^27.12.3",
|
||||
"@push.rocks/smartradius": "^1.3.0",
|
||||
"@push.rocks/smartrequest": "^5.0.3",
|
||||
"@push.rocks/smartrx": "^3.0.10",
|
||||
@@ -71,7 +71,7 @@
|
||||
"@push.rocks/taskbuffer": "^8.0.2",
|
||||
"@serve.zone/catalog": "^2.12.4",
|
||||
"@serve.zone/interfaces": "^5.8.0",
|
||||
"@serve.zone/remoteingress": "^4.22.3",
|
||||
"@serve.zone/remoteingress": "^4.22.4",
|
||||
"@tsclass/tsclass": "^9.5.1",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"lru-cache": "^11.4.0",
|
||||
|
||||
Generated
+10
-10
@@ -84,8 +84,8 @@ importers:
|
||||
specifier: ^4.2.4
|
||||
version: 4.2.4
|
||||
'@push.rocks/smartproxy':
|
||||
specifier: ^27.12.2
|
||||
version: 27.12.2
|
||||
specifier: ^27.12.3
|
||||
version: 27.12.3
|
||||
'@push.rocks/smartradius':
|
||||
specifier: ^1.3.0
|
||||
version: 1.3.0
|
||||
@@ -114,8 +114,8 @@ importers:
|
||||
specifier: ^5.8.0
|
||||
version: 5.8.0
|
||||
'@serve.zone/remoteingress':
|
||||
specifier: ^4.22.3
|
||||
version: 4.22.3
|
||||
specifier: ^4.22.4
|
||||
version: 4.22.4
|
||||
'@tsclass/tsclass':
|
||||
specifier: ^9.5.1
|
||||
version: 9.5.1
|
||||
@@ -1429,8 +1429,8 @@ packages:
|
||||
'@push.rocks/smartpromise@4.2.4':
|
||||
resolution: {integrity: sha512-8FUyYt94hOIY9mqHjitn4h69u0jbEtTF2RKKw2DpiTVFjpDTk9gXbVHZ/V+xEcBrN4mrzdQES0OiDmkNPoddEQ==}
|
||||
|
||||
'@push.rocks/smartproxy@27.12.2':
|
||||
resolution: {integrity: sha512-q97n/UAhfvyds6MhTUAhV5OC7x3Eaot+IN25hW6StyvrxR/odg3/g2UDAJmHoD5X0tKwIhouFd/b8Nwx0p94cg==}
|
||||
'@push.rocks/smartproxy@27.12.3':
|
||||
resolution: {integrity: sha512-nw5+iYhngwrdmSOg87R1opHVZXdLK4GHm/PAtVSWHD7zlnOPhEvdrlJndAq4ehGktf7z6B0SvwwmdrAOCPhWWw==}
|
||||
|
||||
'@push.rocks/smartpuppeteer@2.0.6':
|
||||
resolution: {integrity: sha512-G+8cyDERvbXQcb9Sd8lnYdWYz8b3Mv2LfFf1ULmucDqQhcRHvxrWX/dKsvBZrwKPR4Wg+795Dyd+E1iOOh3tHw==}
|
||||
@@ -1719,8 +1719,8 @@ packages:
|
||||
'@serve.zone/interfaces@5.8.0':
|
||||
resolution: {integrity: sha512-0ekSKUL/b44wmmzuCRANzrjaJRAHtkqiL8cPiMASEs7UJBDqbJCrgtrlJK84pz5dxBz3jTcdznNd5qjB8c6H0A==}
|
||||
|
||||
'@serve.zone/remoteingress@4.22.3':
|
||||
resolution: {integrity: sha512-VUI2VTMHVjju92FXjPe0EQ7op2EyqCr+JQIIGkjxnvqE9aAV9ZtaNzI7y4WwltYNo9rfaa/Bdd8+2EKUYYCD6g==}
|
||||
'@serve.zone/remoteingress@4.22.4':
|
||||
resolution: {integrity: sha512-3SPTlFQQlB7ptdUr0TzZJQ1UOppPWcjcffv25qpO64gzw5f5VhmkywN7YQGAeXqCe4UeuRZrxOwZY0m9SpfJzw==}
|
||||
hasBin: true
|
||||
|
||||
'@smithy/chunked-blob-reader-native@4.2.3':
|
||||
@@ -6696,7 +6696,7 @@ snapshots:
|
||||
|
||||
'@push.rocks/smartpromise@4.2.4': {}
|
||||
|
||||
'@push.rocks/smartproxy@27.12.2':
|
||||
'@push.rocks/smartproxy@27.12.3':
|
||||
dependencies:
|
||||
'@push.rocks/smartcrypto': 2.0.4
|
||||
'@push.rocks/smartlog': 3.2.2
|
||||
@@ -7085,7 +7085,7 @@ snapshots:
|
||||
'@push.rocks/smartlog-interfaces': 3.0.2
|
||||
'@tsclass/tsclass': 9.5.1
|
||||
|
||||
'@serve.zone/remoteingress@4.22.3':
|
||||
'@serve.zone/remoteingress@4.22.4':
|
||||
dependencies:
|
||||
'@push.rocks/qenv': 6.1.4
|
||||
'@push.rocks/smartnftables': 1.2.0
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.41.0',
|
||||
version: '13.41.2',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
+151
-46
@@ -330,6 +330,11 @@ export class DcRouter {
|
||||
public serviceManager: plugins.taskbuffer.ServiceManager;
|
||||
private serviceSubjectSubscription?: plugins.smartrx.rxjs.Subscription;
|
||||
public smartAcmeReady = false;
|
||||
private smartAcmeServiceStarted = false;
|
||||
private smartAcmeStartGeneration = 0;
|
||||
private smartAcmeStartPromise?: Promise<void>;
|
||||
private smartAcmeRetryTimer?: ReturnType<typeof setTimeout>;
|
||||
private smartAcmeRetryAttempt = 0;
|
||||
|
||||
// TypedRouter for API endpoints
|
||||
public typedrouter = new plugins.typedrequest.TypedRouter();
|
||||
@@ -549,45 +554,14 @@ export class DcRouter {
|
||||
.optional()
|
||||
.dependsOn('SmartProxy')
|
||||
.withStart(async () => {
|
||||
if (this.smartAcme) {
|
||||
await this.smartAcme.start();
|
||||
this.smartAcmeReady = true;
|
||||
logger.log('info', 'SmartAcme DNS-01 provider is now ready');
|
||||
|
||||
// Re-trigger certificate provisioning for all auto-cert routes.
|
||||
// During startup, certProvisionFunction returned 'http01' (SmartAcme not ready),
|
||||
// but Rust ACME is disabled when certProvisionFunction is set — so all domains
|
||||
// failed silently (SmartProxy doesn't emit certificate-failed for this path).
|
||||
// Calling updateRoutes() re-triggers provisionCertificatesViaCallback internally,
|
||||
// which calls certProvisionFunction again — now with smartAcmeReady === true.
|
||||
if (this.routeConfigManager) {
|
||||
// Go through RouteConfigManager to get the full merged route set
|
||||
// and serialize via the route-update mutex (prevents stale overwrites)
|
||||
logger.log('info', 'Re-triggering certificate provisioning via RouteConfigManager');
|
||||
this.routeConfigManager.applyRoutes().catch((err: any) => {
|
||||
logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
|
||||
});
|
||||
} else if (this.smartProxy) {
|
||||
// No RouteConfigManager (DB disabled) — re-send current routes to trigger cert provisioning
|
||||
if (this.certProvisionScheduler) {
|
||||
this.certProvisionScheduler.clear();
|
||||
}
|
||||
const currentRoutes = this.smartProxy.routeManager.getRoutes();
|
||||
logger.log('info', `Re-triggering certificate provisioning for ${currentRoutes.length} routes`);
|
||||
this.smartProxy.updateRoutes(currentRoutes).catch((err: any) => {
|
||||
logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.smartAcmeServiceStarted = true;
|
||||
this.startSmartAcmeInBackground();
|
||||
})
|
||||
.withStop(async () => {
|
||||
this.smartAcmeReady = false;
|
||||
if (this.smartAcme) {
|
||||
await this.smartAcme.stop();
|
||||
this.smartAcme = undefined;
|
||||
}
|
||||
this.smartAcmeServiceStarted = false;
|
||||
await this.stopSmartAcme();
|
||||
})
|
||||
.withRetry({ maxRetries: 20, baseDelayMs: 5000, maxDelayMs: 3_600_000, backoffFactor: 2 }),
|
||||
.withRetry({ maxRetries: 0 }),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -778,6 +752,138 @@ export class DcRouter {
|
||||
});
|
||||
}
|
||||
|
||||
private startSmartAcmeInBackground(): void {
|
||||
if (!this.smartAcme) {
|
||||
this.smartAcmeReady = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const generation = ++this.smartAcmeStartGeneration;
|
||||
this.smartAcmeReady = false;
|
||||
this.smartAcmeRetryAttempt = 0;
|
||||
this.clearSmartAcmeRetryTimer();
|
||||
this.scheduleSmartAcmeStart(generation, 0);
|
||||
}
|
||||
|
||||
private scheduleSmartAcmeStart(generation: number, delayMs: number): void {
|
||||
this.clearSmartAcmeRetryTimer();
|
||||
const retryTimer = setTimeout(() => {
|
||||
this.smartAcmeRetryTimer = undefined;
|
||||
this.runSmartAcmeStartAttempt(generation).catch((err) => {
|
||||
logger.log('error', `Unexpected SmartAcme startup error: ${(err as Error).message}`);
|
||||
});
|
||||
}, delayMs);
|
||||
this.smartAcmeRetryTimer = retryTimer;
|
||||
const unrefableTimer = retryTimer as any;
|
||||
if (typeof unrefableTimer?.unref === 'function') {
|
||||
unrefableTimer.unref();
|
||||
}
|
||||
}
|
||||
|
||||
private async runSmartAcmeStartAttempt(generation: number): Promise<void> {
|
||||
const smartAcme = this.smartAcme;
|
||||
if (!smartAcme || generation !== this.smartAcmeStartGeneration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startPromise = smartAcme.start();
|
||||
this.smartAcmeStartPromise = startPromise;
|
||||
|
||||
try {
|
||||
await startPromise;
|
||||
if (generation !== this.smartAcmeStartGeneration || this.smartAcme !== smartAcme) {
|
||||
await smartAcme.stop().catch((err) => {
|
||||
logger.log('warn', `Failed to stop stale SmartAcme instance: ${(err as Error).message}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.smartAcmeReady = true;
|
||||
this.smartAcmeRetryAttempt = 0;
|
||||
logger.log('info', 'SmartAcme DNS-01 provider is now ready');
|
||||
this.retriggerCertificateProvisioningAfterSmartAcmeReady();
|
||||
} catch (err) {
|
||||
if (generation !== this.smartAcmeStartGeneration || this.smartAcme !== smartAcme) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.smartAcmeReady = false;
|
||||
await smartAcme.stop().catch((stopErr) => {
|
||||
logger.log('warn', `Failed to clean up SmartAcme after startup failure: ${(stopErr as Error).message}`);
|
||||
});
|
||||
this.smartAcmeRetryAttempt++;
|
||||
if (this.smartAcmeRetryAttempt > 20) {
|
||||
logger.log('error', `SmartAcme DNS-01 provider failed after 20 startup attempts: ${(err as Error).message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const baseDelayMs = 5000;
|
||||
const maxDelayMs = 3_600_000;
|
||||
const delayMs = Math.min(baseDelayMs * Math.pow(2, this.smartAcmeRetryAttempt - 1), maxDelayMs);
|
||||
const jitter = 0.8 + Math.random() * 0.4;
|
||||
const actualDelayMs = Math.floor(delayMs * jitter);
|
||||
logger.log('warn', `SmartAcme DNS-01 provider startup failed: ${(err as Error).message}; retrying in ${actualDelayMs}ms (attempt ${this.smartAcmeRetryAttempt}/20)`);
|
||||
this.scheduleSmartAcmeStart(generation, actualDelayMs);
|
||||
} finally {
|
||||
if (this.smartAcmeStartPromise === startPromise) {
|
||||
this.smartAcmeStartPromise = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private retriggerCertificateProvisioningAfterSmartAcmeReady(): void {
|
||||
// During startup, certProvisionFunction returns 'http01' while SmartAcme is not ready,
|
||||
// but Rust ACME is disabled when certProvisionFunction is set. Re-applying routes
|
||||
// retries provisioning now that DNS-01 is available.
|
||||
if (this.routeConfigManager) {
|
||||
logger.log('info', 'Re-triggering certificate provisioning via RouteConfigManager');
|
||||
this.routeConfigManager.applyRoutes().catch((err: any) => {
|
||||
logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.smartProxy) {
|
||||
if (this.certProvisionScheduler) {
|
||||
this.certProvisionScheduler.clear();
|
||||
}
|
||||
const currentRoutes = this.smartProxy.routeManager.getRoutes();
|
||||
logger.log('info', `Re-triggering certificate provisioning for ${currentRoutes.length} routes`);
|
||||
this.smartProxy.updateRoutes(currentRoutes).catch((err: any) => {
|
||||
logger.log('warn', `Failed to re-trigger cert provisioning: ${err?.message || err}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private clearSmartAcmeRetryTimer(): void {
|
||||
if (this.smartAcmeRetryTimer) {
|
||||
clearTimeout(this.smartAcmeRetryTimer);
|
||||
this.smartAcmeRetryTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async stopSmartAcme(): Promise<void> {
|
||||
this.smartAcmeStartGeneration++;
|
||||
this.smartAcmeReady = false;
|
||||
this.smartAcmeRetryAttempt = 0;
|
||||
this.clearSmartAcmeRetryTimer();
|
||||
|
||||
const smartAcme = this.smartAcme;
|
||||
if (!smartAcme) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await smartAcme.stop();
|
||||
} catch (err) {
|
||||
logger.log('error', 'Error stopping SmartAcme', { error: String(err) });
|
||||
} finally {
|
||||
if (this.smartAcme === smartAcme) {
|
||||
this.smartAcme = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async start() {
|
||||
await this.checkSystemLimits();
|
||||
logger.log('info', 'Starting DcRouter Services');
|
||||
@@ -1093,17 +1199,13 @@ export class DcRouter {
|
||||
// Initialize cert provision scheduler
|
||||
this.certProvisionScheduler = new CertProvisionScheduler();
|
||||
|
||||
// If we have DNS challenge handlers, create SmartAcme instance and wire certProvisionFunction
|
||||
// Note: SmartAcme.start() is NOT called here — it runs as a separate optional service
|
||||
// via the ServiceManager, with aggressive retry for rate-limit resilience.
|
||||
// If we have DNS challenge handlers, create SmartAcme instance and wire certProvisionFunction.
|
||||
// SmartAcme starts in the background because ACME account setup can be slow or rate-limited,
|
||||
// and must not block dcrouter's global startup timeout.
|
||||
if (this.smartAcme) {
|
||||
await this.stopSmartAcme();
|
||||
}
|
||||
if (challengeHandlers.length > 0) {
|
||||
// Stop old SmartAcme if it exists (e.g., during updateSmartProxyConfig)
|
||||
if (this.smartAcme) {
|
||||
this.smartAcmeReady = false;
|
||||
await this.smartAcme.stop().catch(err =>
|
||||
logger.log('error', 'Error stopping old SmartAcme', { error: String(err) })
|
||||
);
|
||||
}
|
||||
// Safe non-null: challengeHandlers.length > 0 implies both dnsManager
|
||||
// and acmeConfig exist (enforced above).
|
||||
this.smartAcme = new plugins.smartacme.SmartAcme({
|
||||
@@ -1113,6 +1215,9 @@ export class DcRouter {
|
||||
challengeHandlers: challengeHandlers,
|
||||
challengePriority: ['dns-01'],
|
||||
});
|
||||
if (this.smartAcmeServiceStarted) {
|
||||
this.startSmartAcmeInBackground();
|
||||
}
|
||||
|
||||
const scheduler = this.certProvisionScheduler;
|
||||
smartProxyConfig.certProvisionFallbackToAcme = false;
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/dcrouter',
|
||||
version: '13.41.0',
|
||||
version: '13.41.2',
|
||||
description: 'A multifaceted routing service handling mail and SMS delivery functions.'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user