Author SHA1 Message Date
sharang 7bcb1dfc38 fix(ci): clear the 2 HIGH browserslist advisories blocking the image build (#26)
ci / shared (push) Successful in 12s
ci / e2e (push) Blocked by required conditions
ci / test (push) Successful in 10m18s
ci / image (push) Successful in 49s
2026-09-02 10:00:36 +00:00
sharang faa6e83513 fix(products): source products and entitlements from the registry, not hard-coded ids (#25)
ci / e2e (push) Blocked by required conditions
ci / shared (push) Failing after 12s
ci / test (push) Successful in 10m15s
ci / image (push) Skipped
2026-09-02 09:47:52 +00:00
4 changed files with 116 additions and 31 deletions
+2 -1
View File
@@ -54,7 +54,8 @@
"nanoid@<3.3.18": ">=3.3.18", "nanoid@<3.3.18": ">=3.3.18",
"postcss@<8.5.18": ">=8.5.18", "postcss@<8.5.18": ">=8.5.18",
"@auth/core@<0.41.3": ">=0.41.3", "@auth/core@<0.41.3": ">=0.41.3",
"sharp@<0.35.0": ">=0.35.0" "sharp@<0.35.0": ">=0.35.0",
"browserslist@<4.28.7": ">=4.28.7"
} }
} }
} }
+35 -20
View File
@@ -9,6 +9,7 @@ overrides:
postcss@<8.5.18: '>=8.5.18' postcss@<8.5.18: '>=8.5.18'
'@auth/core@<0.41.3': '>=0.41.3' '@auth/core@<0.41.3': '>=0.41.3'
sharp@<0.35.0: '>=0.35.0' sharp@<0.35.0: '>=0.35.0'
browserslist@<4.28.7: '>=4.28.7'
importers: importers:
@@ -1245,6 +1246,11 @@ packages:
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}
hasBin: true hasBin: true
baseline-browser-mapping@2.11.20:
resolution: {integrity: sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==}
engines: {node: '>=6.0.0'}
hasBin: true
brace-expansion@1.1.14: brace-expansion@1.1.14:
resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==}
@@ -1259,8 +1265,8 @@ packages:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'} engines: {node: '>=8'}
browserslist@4.28.2: browserslist@4.28.8:
resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true hasBin: true
@@ -1287,6 +1293,9 @@ packages:
caniuse-lite@1.0.30001793: caniuse-lite@1.0.30001793:
resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==}
caniuse-lite@1.0.30001810:
resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==}
chai@5.3.3: chai@5.3.3:
resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1403,8 +1412,8 @@ packages:
eastasianwidth@0.2.0: eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
electron-to-chromium@1.5.357: electron-to-chromium@1.5.420:
resolution: {integrity: sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==} resolution: {integrity: sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==}
emoji-regex@8.0.0: emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -1557,6 +1566,7 @@ packages:
eslint@9.15.0: eslint@9.15.0:
resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==} resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true hasBin: true
peerDependencies: peerDependencies:
jiti: '*' jiti: '*'
@@ -2199,8 +2209,9 @@ packages:
resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
node-releases@2.0.44: node-releases@2.0.54:
resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==}
engines: {node: '>=18'}
oauth4webapi@3.8.6: oauth4webapi@3.8.6:
resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==} resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==}
@@ -2682,11 +2693,11 @@ packages:
until-async@3.0.2: until-async@3.0.2:
resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==}
update-browserslist-db@1.2.3: update-browserslist-db@1.3.2:
resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
browserslist: '>= 4.21.0' browserslist: '>=4.28.7'
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
@@ -2875,7 +2886,7 @@ snapshots:
dependencies: dependencies:
'@babel/compat-data': 7.29.3 '@babel/compat-data': 7.29.3
'@babel/helper-validator-option': 7.27.1 '@babel/helper-validator-option': 7.27.1
browserslist: 4.28.2 browserslist: 4.28.8
lru-cache: 5.1.1 lru-cache: 5.1.1
semver: 6.3.1 semver: 6.3.1
@@ -3846,6 +3857,8 @@ snapshots:
baseline-browser-mapping@2.10.31: {} baseline-browser-mapping@2.10.31: {}
baseline-browser-mapping@2.11.20: {}
brace-expansion@1.1.14: brace-expansion@1.1.14:
dependencies: dependencies:
balanced-match: 1.0.2 balanced-match: 1.0.2
@@ -3863,13 +3876,13 @@ snapshots:
dependencies: dependencies:
fill-range: 7.1.1 fill-range: 7.1.1
browserslist@4.28.2: browserslist@4.28.8:
dependencies: dependencies:
baseline-browser-mapping: 2.10.31 baseline-browser-mapping: 2.11.20
caniuse-lite: 1.0.30001793 caniuse-lite: 1.0.30001810
electron-to-chromium: 1.5.357 electron-to-chromium: 1.5.420
node-releases: 2.0.44 node-releases: 2.0.54
update-browserslist-db: 1.2.3(browserslist@4.28.2) update-browserslist-db: 1.3.2(browserslist@4.28.8)
cac@6.7.14: {} cac@6.7.14: {}
@@ -3894,6 +3907,8 @@ snapshots:
caniuse-lite@1.0.30001793: {} caniuse-lite@1.0.30001793: {}
caniuse-lite@1.0.30001810: {}
chai@5.3.3: chai@5.3.3:
dependencies: dependencies:
assertion-error: 2.0.1 assertion-error: 2.0.1
@@ -4003,7 +4018,7 @@ snapshots:
eastasianwidth@0.2.0: {} eastasianwidth@0.2.0: {}
electron-to-chromium@1.5.357: {} electron-to-chromium@1.5.420: {}
emoji-regex@8.0.0: {} emoji-regex@8.0.0: {}
@@ -4946,7 +4961,7 @@ snapshots:
object.entries: 1.1.9 object.entries: 1.1.9
semver: 6.3.1 semver: 6.3.1
node-releases@2.0.44: {} node-releases@2.0.54: {}
oauth4webapi@3.8.6: {} oauth4webapi@3.8.6: {}
@@ -5537,9 +5552,9 @@ snapshots:
until-async@3.0.2: {} until-async@3.0.2: {}
update-browserslist-db@1.2.3(browserslist@4.28.2): update-browserslist-db@1.3.2(browserslist@4.28.8):
dependencies: dependencies:
browserslist: 4.28.2 browserslist: 4.28.8
escalade: 3.2.0 escalade: 3.2.0
picocolors: 1.1.1 picocolors: 1.1.1
+14 -5
View File
@@ -107,13 +107,22 @@ export default async function ProductsPage({
title="Findings across products" title="Findings across products"
tail={ tail={
<span className="row" style={{ gap: 6 }}> <span className="row" style={{ gap: 6 }}>
{["all", "compliance-scanner", "certifai"].map((o) => ( {/* Derived from the tenant's own products, not a hard-coded list —
a hard-coded id (e.g. "compliance-scanner") silently filters to
nothing when the registry calls the product something else. */}
<Link
href={`/${slug}/products`}
className={"btn btn-sm" + (productFilter === "all" ? " btn-primary" : "")}
>
All
</Link>
{t.products.map((p) => (
<Link <Link
key={o} key={p.id}
href={o === "all" ? `/${slug}/products` : `/${slug}/products?p=${o}`} href={`/${slug}/products?p=${p.slug}`}
className={"btn btn-sm" + (productFilter === o ? " btn-primary" : "")} className={"btn btn-sm" + (productFilter === p.slug ? " btn-primary" : "")}
> >
{o === "all" ? "All" : o === "certifai" ? "CERTifAI" : "Scanner"} {p.name}
</Link> </Link>
))} ))}
</span> </span>
+65 -5
View File
@@ -10,11 +10,51 @@
// Once the registry is enriched to carry the design fields end-to-end this // Once the registry is enriched to carry the design fields end-to-end this
// module collapses into a thin pass-through. // module collapses into a thin pass-through.
import { tenantBySlug, type TenantRecord } from "@/lib/fixtures"; import { tenantBySlug, type ProductDef, type TenantRecord } from "@/lib/fixtures";
import { fetchTenantBySlug, type Tenant } from "@/lib/tenant-registry"; import {
fetchCatalog,
fetchEntitlements,
fetchTenantBySlug,
type CatalogEntry,
type Entitlement,
type Tenant,
} from "@/lib/tenant-registry";
export type PortalTenant = TenantRecord; export type PortalTenant = TenantRecord;
/** Registry catalog entry -> the shape the product screens render.
*
* The registry `key` becomes both `id` and `slug`, deliberately. The screens
* compare `entitled.includes(p.id)`, and `entitled` is built from the SAME
* registry keys below — so the two sides cannot drift apart. Hard-coding a
* product list here is what previously made a real entitlement (`compliance`)
* fail to match a hand-written product id (`compliance-scanner`).
*
* `frameworks` is empty because the registry contract does not carry it
* (PRODUCT_INTEGRATION_SPEC has products publish a manifest later); the UI
* already renders an empty list without complaint.
*/
function productFromCatalog(entry: CatalogEntry): ProductDef {
return {
id: entry.key,
slug: entry.key,
name: entry.name,
mono: entry.name.replace(/[^A-Za-z]/g, "").slice(0, 2).toUpperCase() || "??",
status: "live",
blurb: entry.description,
frameworks: [],
};
}
/** Entitlements the tenant may actually use right now. */
function activeEntitlements(items: Entitlement[], nowMs: number): Entitlement[] {
return items.filter((e) => {
if (!e.enabled) return false;
if (!e.expires_at) return true;
return new Date(e.expires_at).getTime() > nowMs;
});
}
export async function loadTenantForShell(slug: string): Promise<PortalTenant | null> { export async function loadTenantForShell(slug: string): Promise<PortalTenant | null> {
const fx = tenantBySlug(slug); const fx = tenantBySlug(slug);
if (fx) return fx; if (fx) return fx;
@@ -30,6 +70,26 @@ export async function loadTenantForShell(slug: string): Promise<PortalTenant | n
} }
if (!live) return null; if (!live) return null;
// Entitlements and catalog are what make a started trial actually show as
// entitled. Both are best-effort: the shell must still render if the
// registry answers the tenant but not these, so a failure degrades to an
// empty product grid rather than a 404 on the whole page.
const now = Date.now();
const [catalog, entitlements] = await Promise.all([
fetchCatalog().catch(() => [] as CatalogEntry[]),
fetchEntitlements(live.id).catch(() => [] as Entitlement[]),
]);
const active = activeEntitlements(entitlements, now);
const entitled = active.map((e) => e.product);
const trialing = active
.filter((e) => Boolean(e.expires_at))
.map((e) => e.product);
// Show the catalog, with the entitled ones first so the grid leads with what
// the tenant can actually open.
const products = catalog
.map(productFromCatalog)
.sort((a, b) => Number(entitled.includes(b.id)) - Number(entitled.includes(a.id)));
// Minimal shim so the shell can render. Design-rich fields fall back to // Minimal shim so the shell can render. Design-rich fields fall back to
// placeholders that won't blow up the layout. // placeholders that won't blow up the layout.
return { return {
@@ -51,12 +111,12 @@ export async function loadTenantForShell(slug: string): Promise<PortalTenant | n
contactEmail: "—", contactEmail: "—",
renewal: "—", renewal: "—",
since: live.created_at?.slice(0, 10) ?? "—", since: live.created_at?.slice(0, 10) ?? "—",
entitled: [], entitled,
trialing: [], trialing,
trialEnds: live.trial_ends_at ?? undefined, trialEnds: live.trial_ends_at ?? undefined,
seed: 0, seed: 0,
findingCount: 0, findingCount: 0,
products: [], products,
findings: [], findings: [],
activity: [], activity: [],
audit: [], audit: [],