@redsift/products 12.5.12-alpha.2 → 12.5.12-alpha.3

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.
@@ -1291,6 +1291,7 @@ var enUS = {
1291
1291
  "investigate.extracted-email-short": "Extracted from your email",
1292
1292
  "investigate.dkim-evaluation": "DKIM Evaluation",
1293
1293
  "investigate.fcrdns-evaluation": "FCrDNS Evaluation",
1294
+ "investigate.card.bimi.glossary-link": "What does this mean?",
1294
1295
  "investigate.card.spf-passed_md": "**[](SPF#:glossary) authentication passed.**",
1295
1296
  "investigate.card.spf-failed_md": "**[](SPF#:glossary) authentication failed.**",
1296
1297
  "investigate.card.dmarc.error-raw_md": [
@@ -2008,6 +2009,7 @@ var frFR = {
2008
2009
  "investigate.summary.dnssec": "DNSSEC",
2009
2010
  "investigate.summary.dns-lookups": "{count} Recherches DNS",
2010
2011
  "investigate.summary.void-lookups": "{count} Recherches vides",
2012
+ "investigate.card.bimi.glossary-link": "Qu’est-ce que cela signifie ?",
2011
2013
  "investigate.card.spf-passed_md": "**[](SPF#:glossary) authentification réussie.**",
2012
2014
  "investigate.card.spf-failed_md": "**[](SPF#:glossary) authentification échouée.**",
2013
2015
  "investigate.card.dmarc.error-raw_md": [
@@ -7472,9 +7474,273 @@ function renderDisplayValue(value) {
7472
7474
  return value;
7473
7475
  }
7474
7476
 
7477
+ /**
7478
+ * BIMI code → glossary page map.
7479
+ *
7480
+ * Each BIMI finding carries a `checker_codes` schema id from `redsift/event-schemas`
7481
+ * (e.g. `DNS-BIMI-0020`). The glossary publishes one explainer page per code. This
7482
+ * module turns a code into the URL of its page so a card row can offer a
7483
+ * "What does this mean?" link.
7484
+ *
7485
+ * ## Why the map lives here
7486
+ *
7487
+ * Decision D4a of the glossary design: the website deliberately publishes no
7488
+ * code → slug manifest and performs no code → URL resolution. Its side of the
7489
+ * contract is that these slugs are permanent; if one ever has to change, the
7490
+ * website owes a redirect and an updated map here.
7491
+ *
7492
+ * ## Keyed on the code, never the slug or the message text
7493
+ *
7494
+ * The code is the only stable identifier. Titles, severities and statuses drift
7495
+ * (0033 changed under WEB-1155; 0021's grammar changed) and slugs are human text,
7496
+ * so they are the most likely thing to be re-worded later. Row message text is
7497
+ * worse still — it is backend prose that `formatMessageWithUrls()` rewrites before
7498
+ * it reaches the card, and it is not in any translation catalogue.
7499
+ *
7500
+ * ## Unmapped codes resolve to nothing, by design
7501
+ *
7502
+ * `resolveBimiGlossaryHref` returns `undefined` rather than guessing a URL. This is
7503
+ * an everyday path, not an edge case:
7504
+ * - `DNS-BIMI-0003` has no glossary page — the authored set jumps 0002 → 0004.
7505
+ * - `msgCode` is a free-form string upstream; real payloads carry non-schema
7506
+ * values such as `multiple-records`, `config-error` and `DNS-BIMI-0000-MISMATCH`.
7507
+ * - IPForensics currently mis-assigns some codes (`bimi-error-invalid-record` is
7508
+ * tagged 0021, which is the "both l and a tags missed" page). A permissive
7509
+ * resolver would send a reader to a page describing a *different* error, which
7510
+ * is worse than offering no link at all.
7511
+ *
7512
+ * @see https://redsift.atlassian.net/browse/DS-113
7513
+ */
7514
+
7515
+ /** Production origin for glossary pages, used when the card renders outside the website. */
7516
+ const GLOSSARY_ORIGIN = 'https://redsift.com';
7517
+
7518
+ /** Site-relative path prefix for glossary pages. */
7519
+ const GLOSSARY_PATH = '/glossary';
7520
+
7521
+ /**
7522
+ * Code → glossary slug. Read from the published Prismic API on 2026-08-18.
7523
+ *
7524
+ * 36 entries. Regenerate from the content API rather than editing by hand, and
7525
+ * note the two known coverage gaps:
7526
+ * - 0003 is absent (no authored page).
7527
+ * - 0023–0032 are authored and live but the BIMI Checker does not emit them yet,
7528
+ * so they only appear on cards fed directly from the schema feed.
7529
+ */
7530
+ const BIMI_CODE_TO_GLOSSARY_SLUG = Object.freeze(Object.assign(Object.create(null), {
7531
+ 'DNS-BIMI-0001': 'logo-url-scheme-is-not-https-missing-vmc-in-record',
7532
+ 'DNS-BIMI-0002': 'no-vmc-in-record',
7533
+ // 0003 intentionally absent — no glossary page authored.
7534
+ 'DNS-BIMI-0004': 'missing-l-tag',
7535
+ 'DNS-BIMI-0005': 'failed-to-fetch-logo',
7536
+ 'DNS-BIMI-0006': 'no-version-in-record',
7537
+ 'DNS-BIMI-0007': 'failed-to-fetch-authority-certificate',
7538
+ 'DNS-BIMI-0008': 'no-record',
7539
+ 'DNS-BIMI-0009': 'invalid-record',
7540
+ 'DNS-BIMI-0010': 'valid-record-but-dmarc-isn-t-ready',
7541
+ 'DNS-BIMI-0011': 'vmc-validation-errors',
7542
+ 'DNS-BIMI-0012': 'certificate-can-t-be-accessed-automatically',
7543
+ 'DNS-BIMI-0013': 'declination-to-publish',
7544
+ 'DNS-BIMI-0014': 'certificate-not-valid-for-the-domain',
7545
+ 'DNS-BIMI-0015': 'bimi-record-validation-errors',
7546
+ 'DNS-BIMI-0016': 'authority-url-scheme-is-not-https',
7547
+ 'DNS-BIMI-0017': 'logo-validation-errors',
7548
+ 'DNS-BIMI-0018': 'bimi-record-found-on-organizational-domain',
7549
+ 'DNS-BIMI-0019': 'multiple-bimi-records-found-on-target-domain',
7550
+ 'DNS-BIMI-0020': 'duplicate-tag-detected-in-bimi-record',
7551
+ 'DNS-BIMI-0021': 'both-l-and-a-tags-missed',
7552
+ 'DNS-BIMI-0022': 'no-bimi-record-found-and-dmarc-policy-is-insufficient',
7553
+ 'DNS-BIMI-0023': 'svg-logo-dimensions-use-non-pixel-values',
7554
+ 'DNS-BIMI-0024': 'svg-logo-dimensions-below-96x96-pixel-minimum',
7555
+ 'DNS-BIMI-0025': 'svg-logo-missing-width-or-height-attributes',
7556
+ 'DNS-BIMI-0026': 'vmc-pem-starts-with-utf-8-bom',
7557
+ 'DNS-BIMI-0027': 'vmc-pem-bundle-contains-only-the-leaf-certificate',
7558
+ 'DNS-BIMI-0028': 'vmc-certificate-chain-signature-verification-failed',
7559
+ 'DNS-BIMI-0029': 'vmc-certificate-chain-does-not-terminate-at-a-trusted-bimi-root-ca',
7560
+ 'DNS-BIMI-0030': 'vmc-certificate-chain-ordering-does-not-match-expected-issuance-order',
7561
+ 'DNS-BIMI-0031': 'vmc-pem-bundle-omits-the-root-ca-certificate',
7562
+ 'DNS-BIMI-0032': 'non-https-url-encountered-while-fetching-the-vmc-certificate',
7563
+ 'DNS-BIMI-0033': 'vmc-leaf-certificate-selection-is-ambiguous',
7564
+ 'DNS-BIMI-0034': 'timed-out-fetching-the-bimi-asset',
7565
+ 'DNS-BIMI-0035': 'bimi-asset-host-could-not-be-resolved',
7566
+ 'DNS-BIMI-0036': 'connection-to-the-bimi-asset-host-failed',
7567
+ 'DNS-BIMI-0037': 'tls-handshake-failed-fetching-the-bimi-asset'
7568
+ }));
7569
+
7570
+ /** Options for {@link resolveBimiGlossaryHref}. */
7571
+
7572
+ /**
7573
+ * Resolve a BIMI schema code to its glossary page URL.
7574
+ *
7575
+ * @param code - A `checker_codes` schema id, e.g. `'DNS-BIMI-0020'`. Anything not
7576
+ * in {@link BIMI_CODE_TO_GLOSSARY_SLUG} resolves to `undefined`.
7577
+ * @returns The page URL, or `undefined` when the code has no glossary page.
7578
+ *
7579
+ * @example
7580
+ * resolveBimiGlossaryHref('DNS-BIMI-0008');
7581
+ * // 'https://redsift.com/glossary/no-record'
7582
+ * resolveBimiGlossaryHref('DNS-BIMI-0008', { relative: true });
7583
+ * // '/glossary/no-record'
7584
+ * resolveBimiGlossaryHref('DNS-BIMI-0003'); // undefined — no page authored
7585
+ * resolveBimiGlossaryHref('config-error'); // undefined — not a schema code
7586
+ */
7587
+ function resolveBimiGlossaryHref(code) {
7588
+ let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
7589
+ if (!code) return undefined;
7590
+
7591
+ // Own-property check, not just a truthiness test. `code` originates from `msgCode`,
7592
+ // which is free-form backend data, so a key like `__proto__` or `constructor` would
7593
+ // otherwise resolve to an inherited value — truthy, and stringified into a URL such
7594
+ // as `/glossary/[object Object]`. Both maps are null-prototyped as well; this is the
7595
+ // second layer, so reverting either one alone cannot reintroduce the hole.
7596
+ // Raised in review on PR #1172.
7597
+ if (!Object.prototype.hasOwnProperty.call(BIMI_CODE_TO_GLOSSARY_SLUG, code)) return undefined;
7598
+ const slug = BIMI_CODE_TO_GLOSSARY_SLUG[code];
7599
+ if (typeof slug !== 'string' || !slug) return undefined;
7600
+ const path = `${GLOSSARY_PATH}/${slug}`;
7601
+ return options.relative ? path : `${GLOSSARY_ORIGIN}${path}`;
7602
+ }
7603
+
7604
+ /**
7605
+ * signal-logic translation key → BIMI schema code, for rows that carry no `code`.
7606
+ *
7607
+ * The `bimiRecordStatus` switch in signal-logic builds its rows from a fixed
7608
+ * translation key and never attaches a code, so those rows cannot be linked from
7609
+ * `InfoItem.code` alone (see WEB-1239). The key, however, *is* deterministic: it is
7610
+ * a literal in signal-logic's source, not backend prose, so mapping key → code is
7611
+ * stable in a way that matching the rendered message text is not.
7612
+ *
7613
+ * Covers two families of key:
7614
+ *
7615
+ * 1. The `bimiRecordStatus` switch rows (`record-missing_md` and friends).
7616
+ * 2. The per-error `recordValidationErrors` rows, keyed `card.bimi.errors.<errorCode>`.
7617
+ * These are the individual "Record l value needs to be a valid URL" lines listed
7618
+ * under an invalid record. They matter because they are the rows that actually tell
7619
+ * a reader *what* to fix, and the schema code that IPForensics assigned them is
7620
+ * destroyed before the card sees it: the website's `RecordMessageCodes` rewrites
7621
+ * `DNS-BIMI-00XX` into these `bimi-error-*` names, collapsing 0009/0015/0020/0021
7622
+ * all onto `bimi-error-invalid-record`. Mapping the name back is therefore the only
7623
+ * way to link these rows, and it is safe only where the relation is 1:1.
7624
+ *
7625
+ * Deliberately absent:
7626
+ * - `record-failed_md` — `NOT_VMC`, `WITH_ERRORS` and `CERT_INVALID` all collapse
7627
+ * to this one row, so it is many-to-one and cannot name a single page.
7628
+ * - `record-passed_md` / `record-declination_md` are pass/info states; 0013
7629
+ * (declination) is emitted separately as a coded report row.
7630
+ * - `record-unknown-error_md` has no page.
7631
+ * - `errors.bimi-error-unsupported-values` has no authored glossary page.
7632
+ * - `errors.bimi-error-invalid-record` — the website collapses 0009, 0015, 0020 and
7633
+ * 0021 onto this one name, and those pages describe different errors: 0009 is a
7634
+ * record that cannot be parsed at all, while 0020 (duplicate tag) and 0021 (both
7635
+ * l= and a= missing) are parseable records. Mapping it to 0009 sent a reader
7636
+ * whose actual error was 0020 or 0021 to a page describing a parse failure that
7637
+ * did not happen. Raised in review on PR #1172.
7638
+ * - Most `logoErrors.invalid-svg-*` rows. These describe *which* SVG rule the logo
7639
+ * broke (bad root element, disallowed `script` element, wrong profile, …) and the
7640
+ * glossary has no page per rule — only the umbrella 0017 `logo-validation-errors`.
7641
+ * Pointing 16 distinct failures at one generic page would tell a reader nothing
7642
+ * they cannot already see on the row. `invalid-svg-missing-title` is the exception:
7643
+ * it appears on cards whose only other SVG row is the 0017 summary, so without it
7644
+ * an all-SVG failure card can have no link at all.
7645
+ * - `logoErrors.logo-extract-error` — the website collapses 0034/0035/0036/0037
7646
+ * (timeout, host unresolved, connection failed, TLS handshake failed) all onto it,
7647
+ * and each of those has its own distinct page, so it cannot name one.
7648
+ * - `pemErrors.cert-not-found` — lossy: the website maps 0002+0007 onto it, and
7649
+ * 0034–0037 land there too.
7650
+ */
7651
+ const BIMI_TRANSLATION_KEY_TO_CODE = Object.freeze(Object.assign(Object.create(null), {
7652
+ // bimiRecordStatus switch rows
7653
+ 'card.bimi.record-missing_md': 'DNS-BIMI-0008',
7654
+ // no-record
7655
+ 'card.bimi.record-invalid_md': 'DNS-BIMI-0009',
7656
+ // invalid-record
7657
+ 'card.bimi.record-no-cert_md': 'DNS-BIMI-0007',
7658
+ // failed-to-fetch-authority-certificate
7659
+ // The row's own "[here]" help link is stripped by the adapter, so the glossary page
7660
+ // is the single destination rather than competing with it.
7661
+ 'card.bimi.cert-forbidden_md': 'DNS-BIMI-0012',
7662
+ // certificate-can-t-be-accessed-automatically
7663
+ 'card.bimi.could-not-fetch_md': 'DNS-BIMI-0005',
7664
+ // failed-to-fetch-logo
7665
+ // Per-error recordValidationErrors rows
7666
+ 'card.bimi.errors.bimi-error-missing-l': 'DNS-BIMI-0004',
7667
+ // missing-l-tag
7668
+ 'card.bimi.errors.bimi-error-missing-a': 'DNS-BIMI-0002',
7669
+ // no-vmc-in-record
7670
+ 'card.bimi.errors.bimi-error-missing-v': 'DNS-BIMI-0006',
7671
+ // no-version-in-record
7672
+ 'card.bimi.errors.bimi-error-invalid-l': 'DNS-BIMI-0001',
7673
+ // logo-url-scheme-is-not-https…
7674
+ 'card.bimi.errors.bimi-error-invalid-a': 'DNS-BIMI-0016',
7675
+ // authority-url-scheme-is-not-https
7676
+ 'card.bimi.errors.bimi-error-cert-not-valid': 'DNS-BIMI-0014',
7677
+ // certificate-not-valid-for-the-domain
7678
+ 'card.bimi.errors.bimi-error-incorrect-v': 'DNS-BIMI-0006',
7679
+ // no-version-in-record — v= present but wrong
7680
+ // No page describes an unspecified error, but the row only ever appears under record
7681
+ // validation, and 0015 is that section's umbrella page — better than nothing for a
7682
+ // reader who cannot otherwise tell what to do next.
7683
+ 'card.bimi.errors.bimi-error-unknown': 'DNS-BIMI-0015',
7684
+ // bimi-record-validation-errors
7685
+ // Safe SVG section
7686
+ 'card.bimi.svg-missing-title_md': 'DNS-BIMI-0004',
7687
+ // missing-l-tag — no logo published on the record
7688
+ 'card.bimi.record-svg-added-from-itag-invalid_md': 'DNS-BIMI-0017',
7689
+ // logo-validation-errors
7690
+ // VMC/CMC certificate rows
7691
+ 'card.bimi.vmc-invalid-title_md': 'DNS-BIMI-0011',
7692
+ // vmc-validation-errors
7693
+ 'card.bimi.vmc-not-vmc-title_md': 'DNS-BIMI-0011',
7694
+ // vmc-validation-errors — not a VMC/CMC at all
7695
+ 'card.bimi.vmc.cert-ambiguous-leaf_md': 'DNS-BIMI-0033',
7696
+ // vmc-leaf-certificate-selection-is-ambiguous
7697
+ 'card.bimi.logoErrors.invalid-svg-missing-title_md': 'DNS-BIMI-0017',
7698
+ // logo-validation-errors
7699
+ // signal-logic's catch-all for an svgErrors string it has no translation for. Those
7700
+ // strings are raw fetch/parse failures ("can't fetch SVG from ...: http status not
7701
+ // OK"), which IPForensics tags extractError = DNS-BIMI-0005, so failed-to-fetch-logo
7702
+ // is the page. Without this the row renders with no link at all, since the fallback
7703
+ // branch attaches neither a code nor a per-error key.
7704
+ 'card.bimi.svg-error-fallback_md': 'DNS-BIMI-0005',
7705
+ // failed-to-fetch-logo
7706
+ // IPForensics tags the l=/certificate logo mismatch 'DNS-BIMI-0000-MISMATCH', which is
7707
+ // not a schema code and matches no page, so the code on the item cannot resolve.
7708
+ // The umbrella 0017 page covers this class, and the sibling "image with errors" row
7709
+ // on the same card already points there.
7710
+ 'card.bimi.logoErrors.logo-and-evidence-mismatch_md': 'DNS-BIMI-0017',
7711
+ // logo-validation-errors
7712
+ // VMC/PEM error rows that map 1:1
7713
+ 'card.bimi.pemErrors.cert-not-https_md': 'DNS-BIMI-0016',
7714
+ // authority-url-scheme-is-not-https
7715
+ 'card.bimi.pemErrors.cert-forbidden_md': 'DNS-BIMI-0012',
7716
+ // certificate-can-t-be-accessed-automatically
7717
+ // Matches the `bimi-error-cert-not-valid` row, which resolves to the same page.
7718
+ // Leaving this unlinked made identical "Certificate is not valid" text linked under
7719
+ // BIMI record status but not under VMC/CMC Certificate on the same card.
7720
+ 'card.bimi.pemErrors.cert-not-valid_md': 'DNS-BIMI-0014',
7721
+ // certificate-not-valid-for-the-domain
7722
+ 'card.bimi.pemErrors.cert-bad-signature_md': 'DNS-BIMI-0028' // vmc-certificate-chain-signature-verification-failed
7723
+ }));
7724
+
7725
+ /**
7726
+ * Resolve the BIMI schema code for a signal-logic translation key, for rows that
7727
+ * signal-logic derives from status alone and therefore emits without a `code`.
7728
+ *
7729
+ * @returns The schema code, or `undefined` when the key maps to no single code.
7730
+ */
7731
+ function resolveBimiCodeForTranslationKey(key) {
7732
+ if (!key) return undefined;
7733
+ // Same own-property guard as resolveBimiGlossaryHref: translation keys are literals
7734
+ // today, but this must not become the weak link if that ever changes.
7735
+ if (!Object.prototype.hasOwnProperty.call(BIMI_TRANSLATION_KEY_TO_CODE, key)) return undefined;
7736
+ const code = BIMI_TRANSLATION_KEY_TO_CODE[key];
7737
+ return typeof code === 'string' && code ? code : undefined;
7738
+ }
7739
+
7475
7740
  // =============================================================================
7476
7741
  // BIMI TRANSLATION KEY → data-testid MAPPING
7477
7742
  // =============================================================================
7743
+
7478
7744
  /**
7479
7745
  * Maps signal-logic BIMI translation keys to the data-testid values
7480
7746
  * that the old DS helpers used to set on checklist items.
@@ -7558,17 +7824,106 @@ function adaptExtractedItemCategory(cat) {
7558
7824
  };
7559
7825
  }
7560
7826
 
7827
+ /**
7828
+ * Build the markdown for a row's "What does this mean?" glossary link (DS-113).
7829
+ *
7830
+ * Returns `undefined` when the row carries no code, or a code with no glossary page.
7831
+ * The link is emitted as markdown so the existing `parseGlossaryMarkdown` path in
7832
+ * Checklist renders it — that path already validates the protocol and applies
7833
+ * `target="_blank" rel="noopener noreferrer"`, so this adds no new render surface.
7834
+ *
7835
+ * The reader never sees the raw code: `DNS-BIMI-0024` means nothing to them.
7836
+ */
7837
+ function buildGlossaryLinkMarkdown(code, t) {
7838
+ const href = resolveBimiGlossaryHref(code);
7839
+ if (!href) return undefined;
7840
+ return `[${t('card.bimi.glossary-link')}](${href})`;
7841
+ }
7842
+
7843
+ /**
7844
+ * Markdown links whose target is a "how to fix it" help article, in either of the two
7845
+ * hosts signal-logic links to.
7846
+ *
7847
+ * Matches the whole markdown link plus any leading space, so removing it leaves no gap.
7848
+ * The label is matched loosely (any text) but each href is pinned:
7849
+ * - a bare `community.redsift.com/s/article/<n>` — no `#fragment` branch, so a
7850
+ * glossary *term* link like `[BIMI Record](…/article/000001019#bimi-record)`, which
7851
+ * defines a term mid-sentence rather than offering a fix, is left intact.
7852
+ * - a `knowledge.ondmarc.redsift.com/en/articles/<slug>` — the "[here]" link on the
7853
+ * anti-bot row.
7854
+ */
7855
+ const HELP_ARTICLE_LINK = /\s*\[[^\]]*\]\((?:https:\/\/community\.redsift\.com\/s\/article\/\d+|https:\/\/knowledge\.ondmarc\.redsift\.com\/en\/articles\/[^)]+)\)/g;
7856
+
7857
+ /**
7858
+ * A trailing clause left behind once a help-article link is removed.
7859
+ *
7860
+ * Deliberately **locale-agnostic**: it cuts everything after the last sentence-ending
7861
+ * punctuation rather than matching known lead-in wording. Listing English phrases
7862
+ * would leave every other locale broken — `fr-FR` renders the anti-bot row as
7863
+ * "…anti-bot. En savoir plus et comment résoudre le problème [ici](…)", so an
7864
+ * English-only pattern strips the link and leaves the French clause dangling in front
7865
+ * of the glossary link.
7866
+ *
7867
+ * Requires a preceding sentence to keep: a row that is *only* a lead-in has nothing
7868
+ * to fall back to, so the guard in `stripHelpArticleLink` leaves it alone.
7869
+ */
7870
+ const TRAILING_CLAUSE = /([.!?](?:\*{1,2}|_{1,2}|`)?)[^.!?]*$/;
7871
+
7872
+ /**
7873
+ * Drop a row's help-article link when a glossary link replaces it.
7874
+ *
7875
+ * Both answer the same question, and the glossary page is the canonical explainer —
7876
+ * it covers what the error means *and* how to fix it — so showing both leaves the
7877
+ * reader choosing between two competing links on one line.
7878
+ *
7879
+ * A clause left stranded by the removal goes with it, in any locale. When the row has
7880
+ * no earlier sentence to fall back on, the text is left as-is minus the link rather
7881
+ * than risk emptying the row.
7882
+ */
7883
+ function stripHelpArticleLink(text) {
7884
+ const withoutLink = text.replace(HELP_ARTICLE_LINK, '');
7885
+ if (withoutLink === text) return text;
7886
+ const trimmedEnd = withoutLink.trimEnd();
7887
+ // Nothing after the final sentence break means nothing dangled — keep as-is.
7888
+ if (/[.!?]\s*$/.test(trimmedEnd)) return trimmedEnd;
7889
+ const cut = trimmedEnd.replace(TRAILING_CLAUSE, '$1');
7890
+ return cut === trimmedEnd ? trimmedEnd : cut;
7891
+ }
7892
+
7561
7893
  /**
7562
7894
  * Convert an InfoItem (DisplayValue text) → ChecklistItem-compatible shape.
7895
+ *
7896
+ * When the item carries a `code` (a `checker_codes` schema id forwarded by
7897
+ * signal-logic under WEB-1239) and that code has a glossary page, a
7898
+ * "What does this mean?" link is appended to the end of the row's own text, so it
7899
+ * reads inline rather than as a stacked second line — a card listing several
7900
+ * validation errors would otherwise double in height.
7901
+ *
7902
+ * The link can only be appended inline when the row text is a plain string. A
7903
+ * StructuredData row (charts, status lists, code blocks) has no meaningful "end of
7904
+ * the line" to append to, so those fall back to a subtext beneath the row.
7905
+ *
7906
+ * Where the row already carried a help-article link ("Learn how to fix it", or the
7907
+ * anti-bot row's "[here]"), that link is removed: the glossary page supersedes it, and
7908
+ * two links answering the same question on one line makes the reader choose between
7909
+ * them.
7563
7910
  */
7564
- function adaptInfoItem(item, testId) {
7911
+ function adaptInfoItem(item, testId, t, fallbackCode) {
7912
+ var _item$code;
7913
+ const existing = item.subtexts ? item.subtexts.map(s => ({
7914
+ text: renderDisplayValue(s.text)
7915
+ })) : undefined;
7916
+ // A code carried on the item always wins; the fallback only fills in for rows
7917
+ // signal-logic derives from status alone (DS-113).
7918
+ const link = t ? buildGlossaryLinkMarkdown((_item$code = item.code) !== null && _item$code !== void 0 ? _item$code : fallbackCode, t) : undefined;
7919
+ const inlineable = link !== undefined && typeof item.text === 'string';
7565
7920
  return _objectSpread2({
7566
7921
  type: item.type,
7567
7922
  theme: item.theme,
7568
- text: renderDisplayValue(item.text),
7569
- subtexts: item.subtexts ? item.subtexts.map(s => ({
7570
- text: renderDisplayValue(s.text)
7571
- })) : undefined
7923
+ text: renderDisplayValue(inlineable ? `${stripHelpArticleLink(item.text)} ${link}` : item.text),
7924
+ subtexts: link !== undefined && !inlineable ? [...(existing !== null && existing !== void 0 ? existing : []), {
7925
+ text: renderDisplayValue(link)
7926
+ }] : existing
7572
7927
  }, testId ? {
7573
7928
  'data-testid': testId
7574
7929
  } : undefined);
@@ -7738,12 +8093,14 @@ function adaptBimiExtractor() {
7738
8093
  const [props, t, context] = args;
7739
8094
 
7740
8095
  // Wrap t to track which translation keys map to which returned strings.
7741
- // This lets us stamp data-testid on items by matching the translated text back to its key.
8096
+ // This lets us stamp data-testid on items by matching the translated text back to its key,
8097
+ // and (DS-113) recover the BIMI schema code for status-derived rows that signal-logic
8098
+ // emits without an `InfoItem.code`.
7742
8099
  const translatedValueToKey = new Map();
7743
8100
  const trackingT = (key, params) => {
7744
8101
  const value = t(key, params);
7745
- // Only track keys that have a known testId mapping
7746
- if (BIMI_KEY_TO_TESTID[key]) {
8102
+ // Track keys that carry a testId mapping or a glossary code
8103
+ if (BIMI_KEY_TO_TESTID[key] || resolveBimiCodeForTranslationKey(key)) {
7747
8104
  translatedValueToKey.set(value, key);
7748
8105
  }
7749
8106
  return value;
@@ -7784,11 +8141,49 @@ function adaptBimiExtractor() {
7784
8141
  }
7785
8142
  return undefined;
7786
8143
  };
8144
+
8145
+ // Recover the schema code for a status-derived row (DS-113). signal-logic attaches
8146
+ // `code` only to rows built from a coded error; the `bimiRecordStatus`, validation
8147
+ // error, Safe SVG and VMC-title rows carry none, so fall back to the translation key
8148
+ // that produced the text.
8149
+ //
8150
+ // Mirrors resolveTestId's reverse CMC→VMC pass: signal-logic post-processes several
8151
+ // of these strings with localizeAssertionMarkdown() after t() returns, so the text on
8152
+ // the item no longer matches what the tracking wrapper recorded.
8153
+ const resolveFallbackCode = text => {
8154
+ const direct = resolveBimiCodeForTranslationKey(translatedValueToKey.get(text));
8155
+ if (direct) return direct;
8156
+ const vmcText = text.replace(/CMC/g, 'VMC');
8157
+ if (vmcText !== text) {
8158
+ const viaVmc = resolveBimiCodeForTranslationKey(translatedValueToKey.get(vmcText));
8159
+ if (viaVmc) return viaVmc;
8160
+ }
8161
+ return undefined;
8162
+ };
8163
+
8164
+ // A row whose message contains a URL is not a string: signal-logic runs it through
8165
+ // formatMessageWithUrls(), which returns FormattedText StructuredData with the text
8166
+ // split into segments. Flatten those segments back to a plain string so the key
8167
+ // lookups above can see rows like the svgErrors fallback ("can't fetch SVG from
8168
+ // <url>: http status not OK"), which would otherwise never resolve a code.
8169
+ const plainText = value => {
8170
+ if (typeof value === 'string') return value;
8171
+ if (!value || typeof value !== 'object') return undefined;
8172
+ const segments = value.segments;
8173
+ if (!Array.isArray(segments)) return undefined;
8174
+ const joined = segments.map(s => {
8175
+ var _s$text;
8176
+ return (_s$text = s === null || s === void 0 ? void 0 : s.text) !== null && _s$text !== void 0 ? _s$text : '';
8177
+ }).join('');
8178
+ return joined || undefined;
8179
+ };
7787
8180
  const adaptWithTestIds = categories => categories === null || categories === void 0 ? void 0 : categories.map(cat => ({
7788
8181
  title: cat.title,
7789
8182
  items: cat.items.map(item => {
7790
- const testId = typeof item.text === 'string' ? resolveTestId(item.text) : undefined;
7791
- return adaptInfoItem(item, testId);
8183
+ const text = plainText(item.text);
8184
+ const testId = text !== undefined ? resolveTestId(text) : undefined;
8185
+ const fallbackCode = text !== undefined ? resolveFallbackCode(text) : undefined;
8186
+ return adaptInfoItem(item, testId, t, fallbackCode);
7792
8187
  })
7793
8188
  }));
7794
8189
  return {
@@ -8672,20 +9067,43 @@ const buildPublishedCertificateSections = (cert, pem, t, now, assertionMarkType)
8672
9067
 
8673
9068
  // --- Verification issues ---
8674
9069
  // Sourced from PEM evidence errors plus an ambiguous leaf-selection signal.
8675
- const errorMessages = ((_pem$errors = pem === null || pem === void 0 ? void 0 : pem.errors) !== null && _pem$errors !== void 0 ? _pem$errors : []).map(e => typeof e === 'string' ? e : e === null || e === void 0 ? void 0 : e.message).filter(m => !!m);
9070
+ //
9071
+ // These rows are built here rather than in the extractor adapter, so the adapter's
9072
+ // glossary-link pass (DS-113) does not reach them. Append the link locally, keyed on
9073
+ // the same code the adapter uses, so a verification issue is explained exactly as the
9074
+ // equivalent row in the legacy VMC section is.
9075
+ const withGlossaryLink = (text, code) => {
9076
+ const href = resolveBimiGlossaryHref(code);
9077
+ return href ? `${text} [${t('card.bimi.glossary-link')}](${href})` : text;
9078
+ };
9079
+
9080
+ // pem.errors entries carry a code alongside the message (IPForensics BimiError);
9081
+ // keep it so the row can resolve its page rather than dropping to message-only.
9082
+ const errorEntries = ((_pem$errors = pem === null || pem === void 0 ? void 0 : pem.errors) !== null && _pem$errors !== void 0 ? _pem$errors : []).map(e => typeof e === 'string' ? {
9083
+ message: e,
9084
+ code: undefined
9085
+ } : {
9086
+ message: e === null || e === void 0 ? void 0 : e.message,
9087
+ code: e === null || e === void 0 ? void 0 : e.code
9088
+ }).filter(e => !!e.message);
8676
9089
  const isAmbiguous = (pem === null || pem === void 0 ? void 0 : pem.leafSelectionMethod) === 'AMBIGUOUS';
8677
9090
  const issueItems = [];
8678
9091
  if (isAmbiguous) {
9092
+ const key = 'card.bimi.vmc.cert-ambiguous-leaf_md';
8679
9093
  issueItems.push({
8680
9094
  type: 'warning',
8681
- text: t('card.bimi.vmc.cert-ambiguous-leaf_md'),
9095
+ text: withGlossaryLink(t(key), resolveBimiCodeForTranslationKey(key)),
8682
9096
  'data-testid': 'bimi-cert-issue-ambiguous'
8683
9097
  });
8684
9098
  }
8685
- errorMessages.forEach((message, idx) => {
9099
+ errorEntries.forEach((_ref2, idx) => {
9100
+ let {
9101
+ message,
9102
+ code
9103
+ } = _ref2;
8686
9104
  issueItems.push({
8687
9105
  type: 'warning',
8688
- text: message,
9106
+ text: withGlossaryLink(message, code),
8689
9107
  'data-testid': `bimi-cert-issue-${idx}`
8690
9108
  });
8691
9109
  });