Add page-view analytics, product counters, SSL expiry, and business renewal.
Auto-count storefront page views with 6-month event retention and lifetime counters, track product visit/cart/sale stats, persist SSL cert expiry with daily probes, and support owner titles plus annual contract renewal. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
6cd0b009ef
commit
92601ae729
+67
-21
@@ -2,8 +2,13 @@ import * as tls from 'tls';
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 4000;
|
||||
|
||||
export type TlsProbeResult = {
|
||||
ok: boolean;
|
||||
/** Certificate notAfter when the probe succeeded and a peer cert was present. */
|
||||
expiresAt: Date | null;
|
||||
};
|
||||
|
||||
function peerMatchesHost(host: string, socket: tls.TLSSocket): boolean {
|
||||
// detailed=true so SAN is present for checkServerIdentity
|
||||
const cert = socket.getPeerCertificate(true);
|
||||
if (!cert || Object.keys(cert).length === 0) {
|
||||
return false;
|
||||
@@ -16,26 +21,43 @@ function peerMatchesHost(host: string, socket: tls.TLSSocket): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
function readCertExpiry(socket: tls.TLSSocket): Date | null {
|
||||
const cert = socket.getPeerCertificate(true);
|
||||
if (!cert?.valid_to) return null;
|
||||
const expiresAt = new Date(cert.valid_to);
|
||||
return Number.isNaN(expiresAt.getTime()) ? null : expiresAt;
|
||||
}
|
||||
|
||||
/** Earliest non-null expiry among successful probe results (or null). */
|
||||
export function earliestTlsExpiry(
|
||||
...results: Array<TlsProbeResult | null | undefined>
|
||||
): Date | null {
|
||||
const times = results
|
||||
.filter((r): r is TlsProbeResult => !!r?.ok && !!r.expiresAt)
|
||||
.map((r) => r.expiresAt!.getTime());
|
||||
if (times.length === 0) return null;
|
||||
return new Date(Math.min(...times));
|
||||
}
|
||||
|
||||
/**
|
||||
* Live TLS check: connect to host:443 with SNI and require a trusted cert
|
||||
* whose identity matches the hostname (not merely a valid LE chain for another name).
|
||||
* Returns false on timeout, DNS failure, or cert/hostname errors.
|
||||
*/
|
||||
export function probeTlsHost(
|
||||
export function probeTlsHostDetailed(
|
||||
hostRaw: string,
|
||||
timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
): Promise<boolean> {
|
||||
): Promise<TlsProbeResult> {
|
||||
const host = hostRaw.trim().toLowerCase();
|
||||
if (!host || host.includes(':') || host.endsWith('.local')) {
|
||||
return Promise.resolve(false);
|
||||
return Promise.resolve({ ok: false, expiresAt: null });
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const finish = (ok: boolean) => {
|
||||
const finish = (result: TlsProbeResult) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(ok);
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
let socket: tls.TLSSocket;
|
||||
@@ -49,35 +71,44 @@ export function probeTlsHost(
|
||||
timeout: timeoutMs,
|
||||
},
|
||||
() => {
|
||||
// authorized=true alone is not enough: a default vhost can present
|
||||
// another site's valid cert (ERR_CERT_COMMON_NAME_INVALID in browsers).
|
||||
const ok = socket.authorized === true && peerMatchesHost(host, socket);
|
||||
const ok =
|
||||
socket.authorized === true && peerMatchesHost(host, socket);
|
||||
const expiresAt = ok ? readCertExpiry(socket) : null;
|
||||
socket.destroy();
|
||||
finish(ok);
|
||||
finish({ ok, expiresAt });
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
finish(false);
|
||||
finish({ ok: false, expiresAt: null });
|
||||
return;
|
||||
}
|
||||
|
||||
socket.on('error', () => {
|
||||
socket.destroy();
|
||||
finish(false);
|
||||
finish({ ok: false, expiresAt: null });
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
finish(false);
|
||||
finish({ ok: false, expiresAt: null });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Probe many hosts in parallel (chunked); returns Map host → ssl ok. */
|
||||
export async function probeTlsHosts(
|
||||
/** Boolean wrapper for callers that only need ok/fail. */
|
||||
export async function probeTlsHost(
|
||||
hostRaw: string,
|
||||
timeoutMs = DEFAULT_TIMEOUT_MS,
|
||||
): Promise<boolean> {
|
||||
const result = await probeTlsHostDetailed(hostRaw, timeoutMs);
|
||||
return result.ok;
|
||||
}
|
||||
|
||||
/** Probe many hosts in parallel (chunked); returns Map host → detailed result. */
|
||||
export async function probeTlsHostsDetailed(
|
||||
hosts: Array<string | null | undefined>,
|
||||
chunkSize = 10,
|
||||
): Promise<Map<string, boolean>> {
|
||||
): Promise<Map<string, TlsProbeResult>> {
|
||||
const unique = [
|
||||
...new Set(
|
||||
hosts
|
||||
@@ -86,16 +117,31 @@ export async function probeTlsHosts(
|
||||
),
|
||||
];
|
||||
|
||||
const results = new Map<string, boolean>();
|
||||
const results = new Map<string, TlsProbeResult>();
|
||||
for (let i = 0; i < unique.length; i += chunkSize) {
|
||||
const chunk = unique.slice(i, i + chunkSize);
|
||||
const probed = await Promise.all(
|
||||
chunk.map(async (host) => [host, await probeTlsHost(host)] as const),
|
||||
chunk.map(
|
||||
async (host) => [host, await probeTlsHostDetailed(host)] as const,
|
||||
),
|
||||
);
|
||||
for (const [host, ok] of probed) {
|
||||
results.set(host, ok);
|
||||
for (const [host, result] of probed) {
|
||||
results.set(host, result);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Probe many hosts; returns Map host → ssl ok. */
|
||||
export async function probeTlsHosts(
|
||||
hosts: Array<string | null | undefined>,
|
||||
chunkSize = 10,
|
||||
): Promise<Map<string, boolean>> {
|
||||
const detailed = await probeTlsHostsDetailed(hosts, chunkSize);
|
||||
const results = new Map<string, boolean>();
|
||||
for (const [host, result] of detailed) {
|
||||
results.set(host, result.ok);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user