Fix SSL list badges, labeled AI prompt URLs, and product-category migrate.
Storefront SSL now follows apex expiry instead of requiring www, prompt links include domain and prompt name, and extra color variations merge instead of aborting migrate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
e37cb4a20d
commit
a5f2f8d63b
@@ -99,6 +99,24 @@ export class AiPromptsController {
|
|||||||
return this.service.removeDomainPrompt(domainId, promptId, user);
|
return this.service.removeDomainPrompt(domainId, promptId, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('public/ai-prompts/:host/:name/:prompt/:publicId')
|
||||||
|
@Header('Content-Type', 'text/plain; charset=utf-8')
|
||||||
|
getPublicFull(@Param('publicId') publicId: string) {
|
||||||
|
return this.service.getPublicText(publicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('public/ai-prompts/:host/:name/:publicId')
|
||||||
|
@Header('Content-Type', 'text/plain; charset=utf-8')
|
||||||
|
getPublicLabeled(@Param('publicId') publicId: string) {
|
||||||
|
return this.service.getPublicText(publicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('public/ai-prompts/:host/:publicId')
|
||||||
|
@Header('Content-Type', 'text/plain; charset=utf-8')
|
||||||
|
getPublicHost(@Param('publicId') publicId: string) {
|
||||||
|
return this.service.getPublicText(publicId);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('public/ai-prompts/:publicId')
|
@Get('public/ai-prompts/:publicId')
|
||||||
@Header('Content-Type', 'text/plain; charset=utf-8')
|
@Header('Content-Type', 'text/plain; charset=utf-8')
|
||||||
getPublic(@Param('publicId') publicId: string) {
|
getPublic(@Param('publicId') publicId: string) {
|
||||||
|
|||||||
@@ -40,11 +40,43 @@ export class AiPromptsService {
|
|||||||
throw new BadRequestException('Unable to allocate a public prompt id');
|
throw new BadRequestException('Unable to allocate a public prompt id');
|
||||||
}
|
}
|
||||||
|
|
||||||
private publicUrl(publicId: string): string {
|
private slugSegment(value: string, fallback = 'site'): string {
|
||||||
|
const slug = value
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/['"]/g, '')
|
||||||
|
.replace(/[^a-z0-9\u0600-\u06ff]+/gi, '-')
|
||||||
|
.replace(/-+/g, '-')
|
||||||
|
.replace(/^-+|-+$/g, '')
|
||||||
|
.slice(0, 80);
|
||||||
|
return slug || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private publicUrl(
|
||||||
|
publicId: string,
|
||||||
|
labels?: {
|
||||||
|
host?: string | null;
|
||||||
|
name?: string | null;
|
||||||
|
promptName?: string | null;
|
||||||
|
},
|
||||||
|
): string {
|
||||||
const base =
|
const base =
|
||||||
this.config.get<string>('API_PUBLIC_BASE_URL')?.replace(/\/$/, '') ||
|
this.config.get<string>('API_PUBLIC_BASE_URL')?.replace(/\/$/, '') ||
|
||||||
'https://api.meshkee.com';
|
'https://api.meshkee.com';
|
||||||
return `${base}/api/v1/public/ai-prompts/${publicId}`;
|
const host = labels?.host?.trim().toLowerCase();
|
||||||
|
const nameSlug = labels?.name?.trim()
|
||||||
|
? this.slugSegment(labels.name)
|
||||||
|
: '';
|
||||||
|
const promptSlug = labels?.promptName?.trim()
|
||||||
|
? this.slugSegment(labels.promptName)
|
||||||
|
: '';
|
||||||
|
const parts = [
|
||||||
|
host ? encodeURIComponent(host) : '',
|
||||||
|
nameSlug ? encodeURIComponent(nameSlug) : '',
|
||||||
|
promptSlug ? encodeURIComponent(promptSlug) : '',
|
||||||
|
publicId,
|
||||||
|
].filter(Boolean);
|
||||||
|
return `${base}/api/v1/public/ai-prompts/${parts.join('/')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private serializeTemplate(row: {
|
private serializeTemplate(row: {
|
||||||
@@ -63,7 +95,8 @@ export class AiPromptsService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private serializeDomainPrompt(row: {
|
private serializeDomainPrompt(
|
||||||
|
row: {
|
||||||
id: bigint;
|
id: bigint;
|
||||||
domainId: bigint;
|
domainId: bigint;
|
||||||
sourceTemplateId: bigint | null;
|
sourceTemplateId: bigint | null;
|
||||||
@@ -73,7 +106,12 @@ export class AiPromptsService {
|
|||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}) {
|
},
|
||||||
|
labels?: {
|
||||||
|
host?: string | null;
|
||||||
|
name?: string | null;
|
||||||
|
},
|
||||||
|
) {
|
||||||
return {
|
return {
|
||||||
id: row.id.toString(),
|
id: row.id.toString(),
|
||||||
domainId: row.domainId.toString(),
|
domainId: row.domainId.toString(),
|
||||||
@@ -81,13 +119,29 @@ export class AiPromptsService {
|
|||||||
name: row.name,
|
name: row.name,
|
||||||
body: row.body,
|
body: row.body,
|
||||||
publicId: row.publicId,
|
publicId: row.publicId,
|
||||||
publicUrl: this.publicUrl(row.publicId),
|
publicUrl: this.publicUrl(row.publicId, {
|
||||||
|
host: labels?.host,
|
||||||
|
name: labels?.name,
|
||||||
|
promptName: row.name,
|
||||||
|
}),
|
||||||
sortOrder: row.sortOrder,
|
sortOrder: row.sortOrder,
|
||||||
createdAt: row.createdAt.toISOString(),
|
createdAt: row.createdAt.toISOString(),
|
||||||
updatedAt: row.updatedAt.toISOString(),
|
updatedAt: row.updatedAt.toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async domainLabels(domainId: bigint) {
|
||||||
|
const domain = await this.prisma.domain.findUnique({
|
||||||
|
where: { id: domainId },
|
||||||
|
select: {
|
||||||
|
host: true,
|
||||||
|
business: { select: { name: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!domain) return undefined;
|
||||||
|
return { host: domain.host, name: domain.business.name };
|
||||||
|
}
|
||||||
|
|
||||||
async list(actor: AuthUser) {
|
async list(actor: AuthUser) {
|
||||||
await this.assertSuperAdmin(actor);
|
await this.assertSuperAdmin(actor);
|
||||||
const items = await this.prisma.aiPromptTemplate.findMany({
|
const items = await this.prisma.aiPromptTemplate.findMany({
|
||||||
@@ -163,7 +217,12 @@ export class AiPromptsService {
|
|||||||
domainId: domain.id.toString(),
|
domainId: domain.id.toString(),
|
||||||
host: domain.host,
|
host: domain.host,
|
||||||
businessName: domain.business.name,
|
businessName: domain.business.name,
|
||||||
items: items.map((row) => this.serializeDomainPrompt(row)),
|
items: items.map((row) =>
|
||||||
|
this.serializeDomainPrompt(row, {
|
||||||
|
host: domain.host,
|
||||||
|
name: domain.business.name,
|
||||||
|
}),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +235,11 @@ export class AiPromptsService {
|
|||||||
const domainId = BigInt(domainIdRaw);
|
const domainId = BigInt(domainIdRaw);
|
||||||
const domain = await this.prisma.domain.findUnique({
|
const domain = await this.prisma.domain.findUnique({
|
||||||
where: { id: domainId },
|
where: { id: domainId },
|
||||||
select: { id: true },
|
select: {
|
||||||
|
id: true,
|
||||||
|
host: true,
|
||||||
|
business: { select: { name: true } },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (!domain) throw new NotFoundException('Website not found');
|
if (!domain) throw new NotFoundException('Website not found');
|
||||||
|
|
||||||
@@ -205,7 +268,10 @@ export class AiPromptsService {
|
|||||||
sortOrder: (maxSort._max.sortOrder ?? -1) + 1,
|
sortOrder: (maxSort._max.sortOrder ?? -1) + 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return this.serializeDomainPrompt(created);
|
return this.serializeDomainPrompt(created, {
|
||||||
|
host: domain.host,
|
||||||
|
name: domain.business.name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateDomainPrompt(
|
async updateDomainPrompt(
|
||||||
@@ -230,7 +296,10 @@ export class AiPromptsService {
|
|||||||
...(dto.body !== undefined ? { body: dto.body.trim() } : {}),
|
...(dto.body !== undefined ? { body: dto.body.trim() } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return this.serializeDomainPrompt(updated);
|
return this.serializeDomainPrompt(
|
||||||
|
updated,
|
||||||
|
await this.domainLabels(domainId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeDomainPrompt(
|
async removeDomainPrompt(
|
||||||
|
|||||||
@@ -2869,13 +2869,20 @@ export class LegacyMigrateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load already-migrated variations to stay idempotent.
|
// Load already-migrated variations to stay idempotent.
|
||||||
|
// Meshkee allows at most one color and one size variation per category.
|
||||||
const existingVariations = await this.prisma.categoryVariation.findMany({
|
const existingVariations = await this.prisma.categoryVariation.findMany({
|
||||||
where: { businessId },
|
where: { businessId },
|
||||||
select: { id: true, categoryId: true, name: true },
|
select: { id: true, categoryId: true, name: true, variationType: true },
|
||||||
});
|
});
|
||||||
const existingVarSet = new Set(
|
const existingVarSet = new Set(
|
||||||
existingVariations.map((v) => `${v.categoryId}:${v.name}`),
|
existingVariations.map((v) => `${v.categoryId}:${v.name}`),
|
||||||
);
|
);
|
||||||
|
const variationIdByType = new Map<string, bigint>();
|
||||||
|
for (const v of existingVariations) {
|
||||||
|
if (v.variationType === 'color' || v.variationType === 'size') {
|
||||||
|
variationIdByType.set(`${v.categoryId}:${v.variationType}`, v.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const attr of attributes) {
|
for (const attr of attributes) {
|
||||||
const newCategoryId = oldToNew.get(attr.attributable_id);
|
const newCategoryId = oldToNew.get(attr.attributable_id);
|
||||||
@@ -2883,12 +2890,20 @@ export class LegacyMigrateService {
|
|||||||
|
|
||||||
const variationName = (attr.name ?? '').trim() || 'Variation';
|
const variationName = (attr.name ?? '').trim() || 'Variation';
|
||||||
const varKey = `${newCategoryId}:${variationName}`;
|
const varKey = `${newCategoryId}:${variationName}`;
|
||||||
if (existingVarSet.has(varKey)) continue;
|
|
||||||
|
|
||||||
const variationType = this.legacyAttributeTypeToVariationType(
|
const variationType = this.legacyAttributeTypeToVariationType(
|
||||||
attr.attribute_type,
|
attr.attribute_type,
|
||||||
);
|
);
|
||||||
|
const typeKey =
|
||||||
|
variationType === 'color' || variationType === 'size'
|
||||||
|
? `${newCategoryId}:${variationType}`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
let variationId = typeKey ? variationIdByType.get(typeKey) : undefined;
|
||||||
|
|
||||||
|
if (!variationId && existingVarSet.has(varKey)) continue;
|
||||||
|
|
||||||
|
if (!variationId) {
|
||||||
|
try {
|
||||||
const variation = await this.prisma.categoryVariation.create({
|
const variation = await this.prisma.categoryVariation.create({
|
||||||
data: {
|
data: {
|
||||||
businessId,
|
businessId,
|
||||||
@@ -2899,9 +2914,27 @@ export class LegacyMigrateService {
|
|||||||
},
|
},
|
||||||
select: { id: true },
|
select: { id: true },
|
||||||
});
|
});
|
||||||
|
variationId = variation.id;
|
||||||
existingVarSet.add(varKey);
|
existingVarSet.add(varKey);
|
||||||
|
if (typeKey) variationIdByType.set(typeKey, variationId);
|
||||||
|
} catch {
|
||||||
|
// Unique: one color/size per category, or custom name already exists.
|
||||||
|
if (typeKey) {
|
||||||
|
const existing = await this.prisma.categoryVariation.findFirst({
|
||||||
|
where: { categoryId: newCategoryId, variationType },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
variationId = existing?.id;
|
||||||
|
if (variationId) variationIdByType.set(typeKey, variationId);
|
||||||
|
}
|
||||||
|
if (!variationId) continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const options = valuesByAttr.get(attr.id) ?? [];
|
const options = valuesByAttr.get(attr.id) ?? [];
|
||||||
|
const existingOptionCount = await this.prisma.categoryVariationOption.count(
|
||||||
|
{ where: { variationId } },
|
||||||
|
);
|
||||||
for (const [idx, opt] of options.entries()) {
|
for (const [idx, opt] of options.entries()) {
|
||||||
const label = (opt.name ?? '').trim() || 'Option';
|
const label = (opt.name ?? '').trim() || 'Option';
|
||||||
const value =
|
const value =
|
||||||
@@ -2912,14 +2945,12 @@ export class LegacyMigrateService {
|
|||||||
try {
|
try {
|
||||||
await this.prisma.categoryVariationOption.create({
|
await this.prisma.categoryVariationOption.create({
|
||||||
data: {
|
data: {
|
||||||
variationId: variation.id,
|
variationId,
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
colorHex:
|
colorHex:
|
||||||
variationType === 'color'
|
variationType === 'color' ? this.guessColorHex(label) : null,
|
||||||
? this.guessColorHex(label)
|
sortOrder: existingOptionCount + idx,
|
||||||
: null,
|
|
||||||
sortOrder: idx,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -98,12 +98,14 @@ export class DomainAdminService {
|
|||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const items = rows.map((row) => {
|
const items = rows.map((row) => {
|
||||||
const certStillValid =
|
const expiryMs = row.sslExpiresAt?.getTime() ?? null;
|
||||||
!row.sslExpiresAt || row.sslExpiresAt.getTime() > now;
|
const certStillValid = expiryMs == null || expiryMs > now;
|
||||||
|
// Prefer stored notAfter when present (daily probe is only a writer).
|
||||||
|
const sslEnabled =
|
||||||
|
expiryMs != null ? expiryMs > now : row.sslEnabled && certStillValid;
|
||||||
return {
|
return {
|
||||||
...row,
|
...row,
|
||||||
// List badge: issued/probed OK and cert not past notAfter.
|
sslEnabled,
|
||||||
sslEnabled: row.sslEnabled && certStillValid,
|
|
||||||
sslExpiresAt: row.sslExpiresAt,
|
sslExpiresAt: row.sslExpiresAt,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -208,8 +210,9 @@ export class DomainAdminService {
|
|||||||
? domain.host
|
? domain.host
|
||||||
: `www.${domain.host}`;
|
: `www.${domain.host}`;
|
||||||
const wwwProbe = await probeTlsHostDetailed(wwwHost);
|
const wwwProbe = await probeTlsHostDetailed(wwwHost);
|
||||||
const ok = apexProbe.ok && wwwProbe.ok;
|
const apexOk = apexProbe.ok;
|
||||||
if (ok) {
|
const bothOk = apexOk && wwwProbe.ok;
|
||||||
|
if (apexOk) {
|
||||||
const sslExpiresAt = earliestTlsExpiry(apexProbe, wwwProbe);
|
const sslExpiresAt = earliestTlsExpiry(apexProbe, wwwProbe);
|
||||||
await this.prisma.domain.update({
|
await this.prisma.domain.update({
|
||||||
where: { id: domain.id },
|
where: { id: domain.id },
|
||||||
@@ -218,10 +221,8 @@ export class DomainAdminService {
|
|||||||
...(sslExpiresAt ? { sslExpiresAt } : {}),
|
...(sslExpiresAt ? { sslExpiresAt } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
continue;
|
if (bothOk) continue;
|
||||||
}
|
} else if (domain.sslEnabled) {
|
||||||
|
|
||||||
if (domain.sslEnabled) {
|
|
||||||
await this.prisma.domain.update({
|
await this.prisma.domain.update({
|
||||||
where: { id: domain.id },
|
where: { id: domain.id },
|
||||||
data: { sslEnabled: false, sslExpiresAt: null },
|
data: { sslEnabled: false, sslExpiresAt: null },
|
||||||
@@ -253,23 +254,23 @@ export class DomainAdminService {
|
|||||||
? domain.host
|
? domain.host
|
||||||
: `www.${domain.host}`;
|
: `www.${domain.host}`;
|
||||||
const wwwProbe = await probeTlsHostDetailed(wwwHost);
|
const wwwProbe = await probeTlsHostDetailed(wwwHost);
|
||||||
const wwwOk = apexProbe.ok && wwwProbe.ok;
|
const apexOk = apexProbe.ok;
|
||||||
const sslExpiresAt = wwwOk
|
const sslExpiresAt = apexOk
|
||||||
? earliestTlsExpiry(apexProbe, wwwProbe)
|
? earliestTlsExpiry(apexProbe, wwwProbe)
|
||||||
: null;
|
: null;
|
||||||
await this.prisma.domain.update({
|
await this.prisma.domain.update({
|
||||||
where: { id: domain.id },
|
where: { id: domain.id },
|
||||||
data: {
|
data: {
|
||||||
sslEnabled: wwwOk,
|
sslEnabled: apexOk,
|
||||||
sslExpiresAt,
|
sslExpiresAt,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (wwwOk) {
|
if (apexOk) {
|
||||||
issued.push(domain.host);
|
issued.push(domain.host);
|
||||||
} else {
|
} else {
|
||||||
failed.push({
|
failed.push({
|
||||||
host: domain.host,
|
host: domain.host,
|
||||||
error: 'Certbot finished but TLS probe still failed (apex or www)',
|
error: 'Certbot finished but TLS probe still failed on apex',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -446,22 +447,20 @@ export class DomainAdminService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const storefrontOk = apexOk && wwwOk;
|
// List badge follows apex. www can still fail (no DNS) without marking Invalid.
|
||||||
// Re-probe apex/www once more when already-ok so we persist current cert expiry.
|
if (apexOk && !apexProbe.expiresAt) {
|
||||||
if (storefrontOk && (!apexProbe.expiresAt || !wwwProbe.expiresAt)) {
|
|
||||||
apexProbe = await probeTlsHostDetailed(apex);
|
apexProbe = await probeTlsHostDetailed(apex);
|
||||||
wwwProbe = await probeTlsHostDetailed(wwwHost);
|
|
||||||
apexOk = apexProbe.ok;
|
apexOk = apexProbe.ok;
|
||||||
wwwOk = wwwProbe.ok;
|
|
||||||
}
|
}
|
||||||
const sslExpiresAt =
|
const sslExpiresAt = apexOk
|
||||||
apexOk && wwwOk ? earliestTlsExpiry(apexProbe, wwwProbe) : null;
|
? earliestTlsExpiry(apexProbe, wwwOk ? wwwProbe : apexProbe)
|
||||||
|
: null;
|
||||||
|
|
||||||
const updated = await this.prisma.domain.update({
|
const updated = await this.prisma.domain.update({
|
||||||
where: { id: domainId },
|
where: { id: domainId },
|
||||||
data: {
|
data: {
|
||||||
sslEnabled: storefrontOk && apexOk && wwwOk,
|
sslEnabled: apexOk,
|
||||||
sslExpiresAt: storefrontOk && apexOk && wwwOk ? sslExpiresAt : null,
|
sslExpiresAt: apexOk ? sslExpiresAt : null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class InternalSslController {
|
|||||||
return this.service.listApiHosts();
|
return this.service.listApiHosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Manually probe apex hosts and sync domains.ssl_enabled. */
|
/** Manually probe apex hosts and sync domains.ssl_enabled / ssl_expires_at. */
|
||||||
@Post('refresh-status')
|
@Post('refresh-status')
|
||||||
refreshStatus() {
|
refreshStatus() {
|
||||||
return this.service.refreshSslStatuses('manual');
|
return this.service.refreshSslStatuses('manual');
|
||||||
|
|||||||
@@ -85,8 +85,9 @@ export class InternalSslService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Occasional safety probe (once a day). Issue SSL writes ssl_expires_at and
|
* Occasional safety probe (once a day). Apex TLS is the list source of
|
||||||
* is the main source of truth for the list badge between probes.
|
* truth; a failed probe does not wipe ssl_expires_at while that date is
|
||||||
|
* still in the future. Issue SSL also writes ssl_expires_at.
|
||||||
*/
|
*/
|
||||||
@Cron(CronExpression.EVERY_DAY_AT_4AM)
|
@Cron(CronExpression.EVERY_DAY_AT_4AM)
|
||||||
async refreshSslStatusesCron() {
|
async refreshSslStatusesCron() {
|
||||||
@@ -125,42 +126,45 @@ export class InternalSslService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostsToProbe = domains.flatMap((d) => {
|
// Apex is the list badge. www is optional — missing www DNS used to
|
||||||
const apex = d.host.trim().toLowerCase();
|
// mark every row Invalid and wipe ssl_expires_at.
|
||||||
if (!apex) return [];
|
const hostsToProbe = domains
|
||||||
const www = apex.startsWith('www.') ? apex : `www.${apex}`;
|
.map((d) => d.host.trim().toLowerCase())
|
||||||
return [apex, www];
|
.filter(Boolean);
|
||||||
});
|
|
||||||
const sslByHost = await probeTlsHostsDetailed(hostsToProbe);
|
const sslByHost = await probeTlsHostsDetailed(hostsToProbe);
|
||||||
|
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
const hostKey = domain.host.trim().toLowerCase();
|
const hostKey = domain.host.trim().toLowerCase();
|
||||||
const wwwKey = hostKey.startsWith('www.') ? hostKey : `www.${hostKey}`;
|
|
||||||
const apexResult = sslByHost.get(hostKey) ?? {
|
const apexResult = sslByHost.get(hostKey) ?? {
|
||||||
ok: false,
|
ok: false,
|
||||||
expiresAt: null,
|
expiresAt: null,
|
||||||
};
|
};
|
||||||
const wwwResult = sslByHost.get(wwwKey) ?? {
|
const storedExpiry = domain.sslExpiresAt;
|
||||||
ok: false,
|
const storedStillValid =
|
||||||
expiresAt: null,
|
!!storedExpiry && storedExpiry.getTime() > now.getTime();
|
||||||
};
|
|
||||||
const liveOk = apexResult.ok && wwwResult.ok;
|
|
||||||
const sslExpiresAt = liveOk
|
|
||||||
? earliestTlsExpiry(apexResult, wwwResult)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
const nextEnabled = liveOk;
|
let nextEnabled: boolean;
|
||||||
const nextExpires =
|
let nextExpires: Date | null;
|
||||||
liveOk && sslExpiresAt
|
|
||||||
? sslExpiresAt
|
if (apexResult.ok) {
|
||||||
: liveOk
|
nextEnabled = true;
|
||||||
? domain.sslExpiresAt
|
nextExpires = earliestTlsExpiry(apexResult) ?? storedExpiry;
|
||||||
: null;
|
} else if (storedStillValid) {
|
||||||
|
// Failed probe must not clear a cert that is still in date.
|
||||||
|
nextEnabled = true;
|
||||||
|
nextExpires = storedExpiry;
|
||||||
|
this.logger.warn(
|
||||||
|
`SSL status ${domain.host}: live probe failed; keeping stored expiry ${storedExpiry.toISOString()}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
nextEnabled = false;
|
||||||
|
nextExpires = null;
|
||||||
|
}
|
||||||
|
|
||||||
const enabledChanged = nextEnabled !== domain.sslEnabled;
|
const enabledChanged = nextEnabled !== domain.sslEnabled;
|
||||||
const expiryChanged =
|
const expiryChanged =
|
||||||
(nextExpires?.getTime() ?? null) !==
|
(nextExpires?.getTime() ?? null) !==
|
||||||
(domain.sslExpiresAt?.getTime() ?? null);
|
(storedExpiry?.getTime() ?? null);
|
||||||
|
|
||||||
if (!enabledChanged && !expiryChanged) continue;
|
if (!enabledChanged && !expiryChanged) continue;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user