@push.rocks/smartproxy 27.20.0 → 27.22.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.
@@ -25,6 +25,10 @@ import type { IRustCertificateStatus, IRustChallengeOptions, IRustProxyOptions,
25
25
 
26
26
  type TChallengeProvider = plugins.smartchallenge.IChallengeProvider;
27
27
 
28
+ /** What one certProvisionFunction call amounted to, in the order each sweep logs the tally. */
29
+ const certProvisionOutcomes = ['loaded', 'http01', 'deferred', 'failed', 'stopped', 'unrecognized'] as const;
30
+ type TCertProvisionOutcome = (typeof certProvisionOutcomes)[number];
31
+
28
32
  /**
29
33
  * SmartProxy - Rust-backed proxy engine with TypeScript configuration API.
30
34
  *
@@ -319,7 +323,7 @@ export class SmartProxy extends plugins.EventEmitter {
319
323
  }
320
324
 
321
325
  // Stop metrics polling
322
- this.metricsAdapter.stopPolling();
326
+ await this.metricsAdapter.stopPolling();
323
327
 
324
328
  // Remove exit listener before killing to avoid spurious error events
325
329
  this.bridge.removeAllListeners('exit');
@@ -330,7 +334,7 @@ export class SmartProxy extends plugins.EventEmitter {
330
334
  } catch {
331
335
  // Ignore if already stopped
332
336
  }
333
- this.bridge.kill();
337
+ await this.bridge.terminate();
334
338
 
335
339
  // Stop socket handler relay
336
340
  if (this.socketHandlerServer) {
@@ -489,6 +493,24 @@ export class SmartProxy extends plugins.EventEmitter {
489
493
  return this.bridge.getCertificateStatus(routeName);
490
494
  }
491
495
 
496
+ /**
497
+ * Lift the provisioning failure cooldown of one domain, so the next sweep
498
+ * (`updateRoutes()` or the renewal check) calls `certProvisionFunction` for it
499
+ * again instead of waiting out `certProvisionFailureCooldownMs`. Use it when the
500
+ * cause of a failure was resolved outside SmartProxy, for example after a
501
+ * certificate for the domain was issued out of band.
502
+ *
503
+ * @param domain The provisioning domain exactly as `certProvisionFunction`
504
+ * receives it and `certificate-failed` reports it.
505
+ * @returns true if the domain was cooling down, false if it was not (never
506
+ * failed, or its cooldown had already elapsed).
507
+ */
508
+ public invalidateCertProvisionCooldown(domain: string): boolean {
509
+ const wasCoolingDown = this.isInFailureCooldown(domain, Date.now());
510
+ this.certFailureAt.delete(domain);
511
+ return wasCoolingDown;
512
+ }
513
+
492
514
  /**
493
515
  * Get the metrics interface.
494
516
  */
@@ -582,7 +604,7 @@ export class SmartProxy extends plugins.EventEmitter {
582
604
  // --- Private helpers ---
583
605
 
584
606
  private async cleanupRuntimeResourcesAfterStartFailure(didSpawn: boolean): Promise<void> {
585
- this.metricsAdapter.stopPolling();
607
+ await this.metricsAdapter.stopPolling();
586
608
 
587
609
  if (this.nftablesManager) {
588
610
  await this.nftablesManager.cleanup().catch((err) => {
@@ -616,7 +638,7 @@ export class SmartProxy extends plugins.EventEmitter {
616
638
  if (didSpawn) {
617
639
  this.bridge.removeAllListeners('exit');
618
640
  await this.bridge.stopProxy().catch(() => undefined);
619
- this.bridge.kill();
641
+ await this.bridge.terminate();
620
642
  }
621
643
  }
622
644
 
@@ -985,6 +1007,7 @@ export class SmartProxy extends plugins.EventEmitter {
985
1007
  * For routes with certificate: 'auto', call certProvisionFunction if set.
986
1008
  * If the callback returns a cert object, load it into Rust.
987
1009
  * If it returns 'http01', let Rust handle ACME.
1010
+ * If it returns 'defer', leave the domain for the next sweep.
988
1011
  */
989
1012
  private async provisionCertificatesViaCallback(skipDomains: Set<string> = new Set()): Promise<void> {
990
1013
  const provisionFn = this.settings.certProvisionFunction;
@@ -1045,16 +1068,28 @@ export class SmartProxy extends plugins.EventEmitter {
1045
1068
  const concurrency = this.settings.certProvisionConcurrency ?? 4;
1046
1069
  const semaphore = new ConcurrencySemaphore(concurrency);
1047
1070
 
1048
- const promises = tasks.map(async ({ domain, route }) => {
1071
+ const promises = tasks.map(async ({ domain, route }): Promise<TCertProvisionOutcome> => {
1049
1072
  await semaphore.acquire();
1050
1073
  try {
1051
- await this.provisionSingleDomain(domain, route, provisionFn);
1074
+ return await this.provisionSingleDomain(domain, route, provisionFn);
1052
1075
  } finally {
1053
1076
  semaphore.release();
1054
1077
  }
1055
1078
  });
1056
1079
 
1057
- await Promise.allSettled(promises);
1080
+ // provisionSingleDomain settles provisioning errors itself; it rejects only
1081
+ // when a certificate-failed listener throws, after the failure was recorded.
1082
+ const settled = await Promise.allSettled(promises);
1083
+ const tally = new Map<TCertProvisionOutcome, number>();
1084
+ for (const result of settled) {
1085
+ const outcome = result.status === 'fulfilled' ? result.value : 'failed';
1086
+ tally.set(outcome, (tally.get(outcome) ?? 0) + 1);
1087
+ }
1088
+ const summary = certProvisionOutcomes
1089
+ .filter((outcome) => tally.has(outcome))
1090
+ .map((outcome) => `${tally.get(outcome)} ${outcome}`)
1091
+ .join(', ');
1092
+ logger.log('info', `Cert provisioning sweep finished: ${summary}`, { component: 'smart-proxy' });
1058
1093
  }
1059
1094
 
1060
1095
  /**
@@ -1065,8 +1100,8 @@ export class SmartProxy extends plugins.EventEmitter {
1065
1100
  domain: string,
1066
1101
  route: IRouteConfig,
1067
1102
  provisionFn: (domain: string, eventComms: ICertProvisionEventComms) => Promise<TSmartProxyCertProvisionObject>,
1068
- ): Promise<void> {
1069
- if (this.stopping) return;
1103
+ ): Promise<TCertProvisionOutcome> {
1104
+ if (this.stopping) return 'stopped';
1070
1105
 
1071
1106
  let expiryDate: string | undefined;
1072
1107
  let source = 'certProvisionFunction';
@@ -1088,7 +1123,13 @@ export class SmartProxy extends plugins.EventEmitter {
1088
1123
  `Certificate provisioning timed out for ${domain} after ${timeoutMs}ms`,
1089
1124
  );
1090
1125
 
1091
- if (this.stopping) return;
1126
+ if (this.stopping) return 'stopped';
1127
+
1128
+ // A deferral is not a failure: no cooldown, no event, no ACME fallback.
1129
+ if (result === 'defer') {
1130
+ logger.log('info', `Certificate provisioning deferred for ${domain}; the next sweep retries it`, { component: 'smart-proxy' });
1131
+ return 'deferred';
1132
+ }
1092
1133
 
1093
1134
  if (result === 'http01') {
1094
1135
  if (route.name) {
@@ -1100,11 +1141,11 @@ export class SmartProxy extends plugins.EventEmitter {
1100
1141
  'Note: Rust ACME is disabled when certProvisionFunction is set.', { component: 'smart-proxy' });
1101
1142
  }
1102
1143
  }
1103
- return;
1144
+ return 'http01';
1104
1145
  }
1105
1146
 
1106
1147
  if (result && typeof result === 'object') {
1107
- if (this.stopping) return;
1148
+ if (this.stopping) return 'stopped';
1108
1149
 
1109
1150
  const certObj = result as ISmartProxyCertProvisionCertificate;
1110
1151
  await this.bridge.loadCertificate(
@@ -1135,7 +1176,9 @@ export class SmartProxy extends plugins.EventEmitter {
1135
1176
  expiryDate: expiryDate || (certObj.validUntil ? new Date(certObj.validUntil).toISOString() : undefined),
1136
1177
  source,
1137
1178
  } satisfies ICertificateIssuedEvent);
1179
+ return 'loaded';
1138
1180
  }
1181
+ return 'unrecognized';
1139
1182
  } catch (err: any) {
1140
1183
  logger.log('warn', `certProvisionFunction failed for ${domain}: ${err.message}`, { component: 'smart-proxy' });
1141
1184
 
@@ -1161,6 +1204,7 @@ export class SmartProxy extends plugins.EventEmitter {
1161
1204
  : ' — default self-signed fallback cert will be used'), { component: 'smart-proxy' });
1162
1205
  }
1163
1206
  }
1207
+ return 'failed';
1164
1208
  }
1165
1209
  }
1166
1210