@redsift/products 12.5.12-alpha.3 → 12.5.12-alpha.4
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.
- package/_internal/SignalCardSpfDomain.js +437 -33
- package/_internal/SignalCardSpfDomain.js.map +1 -1
- package/_internal/bimi-checker.js +8 -2
- package/_internal/bimi-checker.js.map +1 -1
- package/bimi-checker.d.ts +2 -14
- package/index.d.ts +4 -16
- package/package.json +3 -3
- package/signal-cards.d.ts +4 -16
|
@@ -539,7 +539,31 @@ var EXTRACTOR_STRINGS = {
|
|
|
539
539
|
"investigate:card.dmarc.full-rejection-subdomain-weak_md": "**100%** of unauthorized emails with the [](FROM#:glossary) `{domain}` will be **rejected**, but subdomains use a weaker policy (sp={sp}), leaving them vulnerable to spoofing.",
|
|
540
540
|
"investigate:card.dmarc.misplaced-bad-policy_md": "**The [FULL](DMARC_POLICY#:glossary) is misplaced and has a syntax error:**",
|
|
541
541
|
"investigate:card.dmarc.misplaced-policy_md": "**The [FULL](DMARC_POLICY#:glossary) is misplaced.**",
|
|
542
|
-
|
|
542
|
+
// WEB-1243: standards-neutral. The previous wording said receivers "stop policy
|
|
543
|
+
// discovery and apply no DMARC policy at all", which is RFC 7489 §6.6.3 step 5 only.
|
|
544
|
+
// RFC 9989 §4.10 receivers discard the duplicates and CONTINUE the tree walk, so an
|
|
545
|
+
// ancestor policy may still apply — and DNS-DMARC-0044, the live signal on current
|
|
546
|
+
// production, is emitted by exactly those receivers. Stating the RFC 7489 behaviour
|
|
547
|
+
// as universal was therefore false for the code that now triggers this message.
|
|
548
|
+
// Rewritten to assert only what is true on both: the outcome is receiver-dependent
|
|
549
|
+
// and therefore unreliable, and the fix is the same either way.
|
|
550
|
+
"investigate:card.dmarc.multiple-policies_md": "**More than one [FULL](DMARC_POLICY#:glossary) is published on `{policyDomain}`.** Handling depends on the receiver: some stop policy discovery and apply no DMARC policy at all, others discard the duplicate records and continue. Your policy cannot be relied on for any recipient while more than one record is published. Publish exactly one DMARC record.",
|
|
551
|
+
// WEB-1243: the UNCONFIRMED counterpart. `indeterminate` means the check could not
|
|
552
|
+
// finish, NOT that duplicates were found. It resolves to a danger status on purpose —
|
|
553
|
+
// an unproven absence must not read as a proven one on a security signal — but the copy
|
|
554
|
+
// must not then assert the conflict as fact. `multiple-policies_md` says duplicates ARE
|
|
555
|
+
// published and to publish exactly one record, which on this path would tell a user
|
|
556
|
+
// with a healthy single record to change it.
|
|
557
|
+
//
|
|
558
|
+
// DELIBERATELY CAUSE-NEUTRAL. `indeterminate` has more than one cause: the scan budget
|
|
559
|
+
// running out, AND a candidate entry the check could not read (a rendered display group
|
|
560
|
+
// reaching a semantic position). An earlier draft said "the domain published more
|
|
561
|
+
// records than this check inspects", which is a FALSE statement of fact on the second
|
|
562
|
+
// cause — a one-name payload got told it had published too many records. Naming any
|
|
563
|
+
// single cause here is wrong unless the reason is carried alongside the verdict, so the
|
|
564
|
+
// copy states only what is true of every path: not everything was inspected, and the
|
|
565
|
+
// reader should go and look.
|
|
566
|
+
"investigate:card.dmarc.multiple-policies-unconfirmed_md": "**We could not fully check how many [FULL](DMARC_POLICY#:glossary) records are published on `{policyDomain}`.** This check did not inspect every record, so we cannot confirm whether more than one is live. If more than one is published, your policy cannot be relied on for any recipient. Review the DNS zone for `{policyDomain}` and confirm it publishes exactly one DMARC record.",
|
|
543
567
|
"investigate:card.dmarc.no-policy_md": "**No [FULL](DMARC_POLICY#:glossary) published on domain**\n\n**`{policyDomain}`**",
|
|
544
568
|
"investigate:card.dmarc.no-txt-published-policy-on-domain_md": "No [](TXT_RECORDS#:glossary) published on `{policyDomain}`",
|
|
545
569
|
"investigate:card.dmarc.permerror-no-rua_md": "No valid reporting URI ([](RUA#:glossary)) is present, so receivers will skip DMARC processing entirely.",
|
|
@@ -1026,7 +1050,8 @@ var EXTRACTOR_STRINGS = {
|
|
|
1026
1050
|
"investigate:card.dmarc.full-rejection-subdomain-weak_md": "**100%** des courriels non autoris\xE9s avec le [](FROM#:glossary) `{domain}` seront **rejet\xE9s**, mais les sous-domaines utilisent une politique plus faible (sp={sp}), les laissant vuln\xE9rables \xE0 l'usurpation.",
|
|
1027
1051
|
"investigate:card.dmarc.misplaced-bad-policy_md": "**Le [FULL](DMARC_POLICY#:glossary) est mal plac\xE9 et a une erreur de syntaxe:**",
|
|
1028
1052
|
"investigate:card.dmarc.misplaced-policy_md": "**Le [FULL](DMARC_POLICY#:glossary) est mal plac\xE9.",
|
|
1029
|
-
"investigate:card.dmarc.multiple-policies_md": "**Plusieurs [FULL](DMARC_POLICY#:glossary) sont publi\xE9es sur `{policyDomain}`.**
|
|
1053
|
+
"investigate:card.dmarc.multiple-policies_md": "**Plusieurs [FULL](DMARC_POLICY#:glossary) sont publi\xE9es sur `{policyDomain}`.** Le traitement d\xE9pend du destinataire : certains interrompent la d\xE9couverte de la politique et n'appliquent aucune politique DMARC, d'autres ignorent les enregistrements en double et continuent. Votre politique ne peut \xEAtre consid\xE9r\xE9e comme fiable pour aucun destinataire tant que plusieurs enregistrements sont publi\xE9s. Publiez un seul enregistrement DMARC.",
|
|
1054
|
+
"investigate:card.dmarc.multiple-policies-unconfirmed_md": "**Nous n'avons pas pu v\xE9rifier enti\xE8rement combien d'enregistrements [FULL](DMARC_POLICY#:glossary) sont publi\xE9s sur `{policyDomain}`.** Cette v\xE9rification n'a pas inspect\xE9 tous les enregistrements, nous ne pouvons donc pas confirmer si plusieurs sont actifs. Si plusieurs sont publi\xE9s, votre politique ne peut \xEAtre consid\xE9r\xE9e comme fiable pour aucun destinataire. Examinez la zone DNS de `{policyDomain}` et confirmez qu'elle publie un seul enregistrement DMARC.",
|
|
1030
1055
|
"investigate:card.dmarc.no-policy_md": "**Aucune [FULL](DMARC_POLICY#:glossary) publi\xE9e sur le domaine**\n\n**`{policyDomain}`**",
|
|
1031
1056
|
"investigate:card.dmarc.no-txt-published-policy-on-domain_md": "Aucun [](TXT_RECORD#:glossary) publi\xE9 sur `{policyDomain}`",
|
|
1032
1057
|
"investigate:card.dmarc.permerror-no-rua_md": "Aucun URI de rapport valide ([](RUA#:glossary)) n'est pr\xE9sent, les destinataires ignoreront enti\xE8rement le traitement DMARC.",
|
|
@@ -1316,6 +1341,7 @@ var enUS = {
|
|
|
1316
1341
|
"investigate.card.dmarc.policy-remaining-emails-pct-reject_md": "The remaining **{remainingPercent}%** of unauthorized emails will be subject to the **quarantine** policy.",
|
|
1317
1342
|
"investigate.card.dmarc.policy-remaining-emails-pct-quarantine_md": "The remaining **{remainingPercent}%** of unauthorized emails will be **delivered normally**.",
|
|
1318
1343
|
"investigate.card.dmarc.no-policy_md": "**No [FULL](DMARC_POLICY#:glossary) published on domain**\n\n**`{policyDomain}`**",
|
|
1344
|
+
"investigate.card.dmarc.multiple-policies_md": "**More than one [FULL](DMARC_POLICY#:glossary) is published on `{policyDomain}`.** Handling depends on the receiver: some stop policy discovery and apply no DMARC policy at all, others discard the duplicate records and continue. Your policy cannot be relied on for any recipient while more than one record is published. Publish exactly one DMARC record.",
|
|
1319
1345
|
"investigate.card.dmarc.dmarc-passed_md": "[](DMARC#:glossary) **passed**",
|
|
1320
1346
|
"investigate.card.dmarc.dmarc-passed-no-rua_md": "[](DMARC#:glossary) is BIMI compliant **BUT** it does not have an **aggregate report address** (rua) entry.",
|
|
1321
1347
|
"investigate.card.dmarc.dmarc-no-rua_md": "[](DMARC#:glossary) does not have an **aggregate report address** (rua) entry.",
|
|
@@ -1935,7 +1961,14 @@ var enUS = {
|
|
|
1935
1961
|
"investigate.card.dmarc.domain-aligned": "The domain is aligned with the From Domain. It can contribute to passing DMARC.",
|
|
1936
1962
|
"investigate.card.dmarc.domain-aligned-not-verified": "The domain is aligned with the From Domain, but its signature did not verify, so it cannot contribute to passing DMARC.",
|
|
1937
1963
|
"investigate.card.dmarc.spf-domain-aligned-not-verified": "The domain is aligned with the From Domain, but SPF did not pass, so it cannot contribute to passing DMARC.",
|
|
1938
|
-
"investigate.card.dmarc.domain-not-aligned": "The domain is not aligned with the From Domain. It cannot contribute to passing DMARC."
|
|
1964
|
+
"investigate.card.dmarc.domain-not-aligned": "The domain is not aligned with the From Domain. It cannot contribute to passing DMARC.",
|
|
1965
|
+
"investigate.card.dmarc.multiple-policies-unconfirmed_md": "**We could not fully check how many [FULL](DMARC_POLICY#:glossary) records are published on `{policyDomain}`.** This check did not inspect every record, so we cannot confirm whether more than one is live. If more than one is published, your policy cannot be relied on for any recipient. Review the DNS zone for `{policyDomain}` and confirm it publishes exactly one DMARC record.",
|
|
1966
|
+
"investigate.card.extracted.truncated-records": "[… {count} more records not shown]",
|
|
1967
|
+
"investigate.card.extracted.truncated-records_one": "[… 1 more record not shown]",
|
|
1968
|
+
"investigate.card.extracted.truncated-records-atleast": "[… at least {count} more records not shown]",
|
|
1969
|
+
"investigate.card.extracted.truncated-records-uninspected": "[… further records not inspected]",
|
|
1970
|
+
"investigate.card.extracted.truncated-record-length": "[… long records shortened for display]",
|
|
1971
|
+
"investigate.card.extracted.truncated-records-atleast_one": "[… at least 1 more record not shown]"
|
|
1939
1972
|
};
|
|
1940
1973
|
|
|
1941
1974
|
var frFR = {
|
|
@@ -2034,6 +2067,7 @@ var frFR = {
|
|
|
2034
2067
|
"investigate.card.dmarc.policy-remaining-emails-pct-reject_md": "Les **{remainingPercent}%** restants de courriels non autorisés seront soumis à la politique de **mise en quarantaine**.",
|
|
2035
2068
|
"investigate.card.dmarc.policy-remaining-emails-pct-quarantine_md": "Les **{remainingPercent}%** restants de courriels non autorisés seront **livrés normalement**.",
|
|
2036
2069
|
"investigate.card.dmarc.no-policy_md": "**Aucune [FULL](DMARC_POLICY#:glossary) publiée sur le domaine**\n\n**`{policyDomain}`**",
|
|
2070
|
+
"investigate.card.dmarc.multiple-policies_md": "**Plusieurs [FULL](DMARC_POLICY#:glossary) sont publiées sur `{policyDomain}`.** Le traitement dépend du destinataire : certains interrompent la découverte de la politique et n'appliquent aucune politique DMARC, d'autres ignorent les enregistrements en double et continuent. Votre politique ne peut être considérée comme fiable pour aucun destinataire tant que plusieurs enregistrements sont publiés. Publiez un seul enregistrement DMARC.",
|
|
2037
2071
|
"investigate.card.dmarc.dmarc-passed_md": "[](DMARC#:glossary) **réussi**",
|
|
2038
2072
|
"investigate.card.dmarc.dmarc-passed-no-rua_md": "[](DMARC#:glossary) est conforme à BIMI **MAIS** n'a pas d'**adresse de rapport agrégé** (rua).",
|
|
2039
2073
|
"investigate.card.dmarc.dmarc-no-rua_md": "[](DMARC#:glossary) n'a pas d'**adresse de rapport agrégé** (rua).",
|
|
@@ -2655,7 +2689,14 @@ var frFR = {
|
|
|
2655
2689
|
"investigate.card.dmarc.domain-aligned": "Le domaine est aligné avec le domaine expéditeur. Il peut contribuer à la réussite de DMARC.",
|
|
2656
2690
|
"investigate.card.dmarc.domain-aligned-not-verified": "Le domaine est aligné avec le domaine expéditeur, mais sa signature n'a pas été vérifiée, il ne peut donc pas contribuer à la réussite de DMARC.",
|
|
2657
2691
|
"investigate.card.dmarc.spf-domain-aligned-not-verified": "Le domaine est aligné avec le domaine expéditeur, mais SPF n'a pas réussi, il ne peut donc pas contribuer à la réussite de DMARC.",
|
|
2658
|
-
"investigate.card.dmarc.domain-not-aligned": "Le domaine n'est pas aligné avec le domaine expéditeur. Il ne peut pas contribuer à la réussite de DMARC."
|
|
2692
|
+
"investigate.card.dmarc.domain-not-aligned": "Le domaine n'est pas aligné avec le domaine expéditeur. Il ne peut pas contribuer à la réussite de DMARC.",
|
|
2693
|
+
"investigate.card.dmarc.multiple-policies-unconfirmed_md": "**Nous n'avons pas pu vérifier entièrement combien d'enregistrements [FULL](DMARC_POLICY#:glossary) sont publiés sur `{policyDomain}`.** Cette vérification n'a pas inspecté tous les enregistrements, nous ne pouvons donc pas confirmer si plusieurs sont actifs. Si plusieurs sont publiés, votre politique ne peut être considérée comme fiable pour aucun destinataire. Examinez la zone DNS de `{policyDomain}` et confirmez qu'elle publie un seul enregistrement DMARC.",
|
|
2694
|
+
"investigate.card.extracted.truncated-records": "[… {count} enregistrements supplémentaires non affichés]",
|
|
2695
|
+
"investigate.card.extracted.truncated-records_one": "[… 1 enregistrement supplémentaire non affiché]",
|
|
2696
|
+
"investigate.card.extracted.truncated-records-atleast": "[… au moins {count} enregistrements supplémentaires non affichés]",
|
|
2697
|
+
"investigate.card.extracted.truncated-records-uninspected": "[… enregistrements supplémentaires non examinés]",
|
|
2698
|
+
"investigate.card.extracted.truncated-record-length": "[… enregistrements longs raccourcis pour l’affichage]",
|
|
2699
|
+
"investigate.card.extracted.truncated-records-atleast_one": "[… au moins 1 enregistrement supplémentaire non affiché]"
|
|
2659
2700
|
};
|
|
2660
2701
|
|
|
2661
2702
|
/**
|
|
@@ -4828,8 +4869,49 @@ const CodeBlock = styled.div`
|
|
|
4828
4869
|
display: none !important;
|
|
4829
4870
|
}
|
|
4830
4871
|
`;
|
|
4872
|
+
|
|
4873
|
+
/**
|
|
4874
|
+
* One logical line inside a `CodeBlock` — typically a single published DNS record.
|
|
4875
|
+
*
|
|
4876
|
+
* DS-115: a domain can publish more than one competing `_dmarc` TXT record, and the card
|
|
4877
|
+
* must show them as distinct records. `CodeBlock` forces `p { display: inline }`, which
|
|
4878
|
+
* flowed them into one unbroken line, so multi-line values use this block-level wrapper
|
|
4879
|
+
* instead of a `<p>`.
|
|
4880
|
+
*/
|
|
4881
|
+
const CodeBlockLine = styled.div`
|
|
4882
|
+
display: block;
|
|
4883
|
+
white-space: pre-wrap;
|
|
4884
|
+
word-break: break-all;
|
|
4885
|
+
|
|
4886
|
+
& + & {
|
|
4887
|
+
margin-top: 4px;
|
|
4888
|
+
}
|
|
4889
|
+
`;
|
|
4890
|
+
|
|
4891
|
+
/**
|
|
4892
|
+
* Renderer commentary about a truncated literal value — NOT zone content.
|
|
4893
|
+
*
|
|
4894
|
+
* DS-115. Deliberately its own component rather than a reused `ExtractionCaption`: that one
|
|
4895
|
+
* is `position: absolute`, anchored to an `ExtractionBox`, so on the paths that have no
|
|
4896
|
+
* positioned ancestor it would escape its container, and inside a box it would land on the
|
|
4897
|
+
* same coordinates as the ordinary caption. This stays in normal flow directly beneath the
|
|
4898
|
+
* code block.
|
|
4899
|
+
*
|
|
4900
|
+
* Visually distinct from `CodeBlockLine` on purpose — italic, not monospace — because a zone
|
|
4901
|
+
* can publish a record whose text imitates this notice, and the two must not be confusable.
|
|
4902
|
+
*/
|
|
4903
|
+
const LiteralTruncationNotice = styled.div`
|
|
4904
|
+
display: block;
|
|
4905
|
+
margin-top: 6px;
|
|
4906
|
+
font-style: italic;
|
|
4907
|
+
font-size: 12px;
|
|
4908
|
+
opacity: 0.75;
|
|
4909
|
+
word-break: break-word;
|
|
4910
|
+
`;
|
|
4831
4911
|
ExtractionBox.displayName = 'ExtractionBox';
|
|
4832
4912
|
ExtractionCaption.displayName = 'ExtractionCaption';
|
|
4913
|
+
CodeBlockLine.displayName = 'CodeBlockLine';
|
|
4914
|
+
LiteralTruncationNotice.displayName = 'LiteralTruncationNotice';
|
|
4833
4915
|
InfoItemsHighlight.displayName = 'InfoItemsHighlight';
|
|
4834
4916
|
|
|
4835
4917
|
// All theming uses CSS variables with Design System fallbacks (Poppins).
|
|
@@ -6642,19 +6724,244 @@ function getIcon(_ref) {
|
|
|
6642
6724
|
size: "small"
|
|
6643
6725
|
}) : null;
|
|
6644
6726
|
}
|
|
6645
|
-
|
|
6727
|
+
|
|
6728
|
+
/**
|
|
6729
|
+
* Lines a literal (code-block) render may emit, shared across ONE CATEGORY.
|
|
6730
|
+
*
|
|
6731
|
+
* Scope is per category, not per card: `ExtractedCategoryContent` creates one budget and
|
|
6732
|
+
* threads it through every item and leaf it renders, so a card with N literal categories can
|
|
6733
|
+
* emit up to N x this figure. That is deliberate — a category is the unit a reader looks at,
|
|
6734
|
+
* and letting one category's pathological payload blank out a sibling section would trade a
|
|
6735
|
+
* bounded render for a misleading one.
|
|
6736
|
+
*
|
|
6737
|
+
* Two earlier attempts were not bounds at all. A per-leaf cap failed because
|
|
6738
|
+
* `renderExtractedValue` recurses into arrays (5,000 one-line entries → 5,004 nodes). A
|
|
6739
|
+
* per-value cap failed because upstream emits one item PER CANDIDATE GROUP, so 20 groups each
|
|
6740
|
+
* got a fresh 50 (→ 1,000 lines, 20 notices).
|
|
6741
|
+
*
|
|
6742
|
+
* Generous for real data: a real `_dmarc` name publishes one record and the pathological
|
|
6743
|
+
* production case is two.
|
|
6744
|
+
*/
|
|
6745
|
+
const MAX_LITERAL_LINES = 50;
|
|
6746
|
+
|
|
6747
|
+
/**
|
|
6748
|
+
* Characters rendered per literal line.
|
|
6749
|
+
*
|
|
6750
|
+
* Count alone does not bound size — 50 lines of 200KB each rendered 10,001,001 characters
|
|
6751
|
+
* (measured). 4096 rather than upstream's 512 context bound: this is the UI, and legitimate
|
|
6752
|
+
* evidence exceeds 512 — a DNS TXT RR concatenates multiple 255-octet strings, and a
|
|
6753
|
+
* 4096-bit DKIM `p=` value alone is ~740 characters. 512 would have silently truncated real
|
|
6754
|
+
* keys, which on a card whose job is showing published records is a correctness bug, not a
|
|
6755
|
+
* safety measure.
|
|
6756
|
+
*/
|
|
6757
|
+
const MAX_LITERAL_LINE_CHARS = 4096;
|
|
6758
|
+
|
|
6759
|
+
/**
|
|
6760
|
+
* Total literal characters per CATEGORY, independent of the per-line cap.
|
|
6761
|
+
*
|
|
6762
|
+
* The per-line cap alone still allows `MAX_LITERAL_LINES × MAX_LITERAL_LINE_CHARS`. This is
|
|
6763
|
+
* the ceiling that actually bounds payload size. Same per-category scope as
|
|
6764
|
+
* `MAX_LITERAL_LINES` — see that note.
|
|
6765
|
+
*/
|
|
6766
|
+
const MAX_LITERAL_TOTAL_CHARS = 64000;
|
|
6767
|
+
|
|
6768
|
+
/**
|
|
6769
|
+
* Array entries VISITED per category, whatever they turn out to contain.
|
|
6770
|
+
*
|
|
6771
|
+
* Distinct from the line budget, which bounds output: entries that contribute nothing —
|
|
6772
|
+
* empty strings, whitespace, nulls — do not consume output quota, so a scan that stopped
|
|
6773
|
+
* only on the output budget could still walk a million entries. Nothing past this is read.
|
|
6774
|
+
*/
|
|
6775
|
+
const MAX_LITERAL_ENTRIES_SCANNED = 2000;
|
|
6776
|
+
|
|
6777
|
+
/**
|
|
6778
|
+
* Shared, mutable literal render budget for one CATEGORY.
|
|
6779
|
+
*
|
|
6780
|
+
* `hiddenLines` is a LOWER BOUND, not an exact count: producing an exact figure would mean
|
|
6781
|
+
* scanning every withheld character of an attacker-controlled value, which is unbounded work
|
|
6782
|
+
* for a number nobody acts on. Copy says "at least N" / "not inspected" accordingly.
|
|
6783
|
+
*/
|
|
6784
|
+
|
|
6785
|
+
const createLiteralBudget = () => ({
|
|
6786
|
+
lines: MAX_LITERAL_LINES,
|
|
6787
|
+
chars: MAX_LITERAL_TOTAL_CHARS,
|
|
6788
|
+
entries: MAX_LITERAL_ENTRIES_SCANNED,
|
|
6789
|
+
hiddenLines: 0,
|
|
6790
|
+
hiddenUncounted: false,
|
|
6791
|
+
shortenedLines: false
|
|
6792
|
+
});
|
|
6793
|
+
const budgetExhausted = b => b.lines <= 0 || b.chars <= 0;
|
|
6794
|
+
|
|
6795
|
+
/**
|
|
6796
|
+
* Truncate without splitting a surrogate pair.
|
|
6797
|
+
*
|
|
6798
|
+
* A bare `slice` cuts UTF-16 code units, so a boundary landing between a high and low
|
|
6799
|
+
* surrogate emits a replacement glyph — a rendering artifact in what is supposed to be a
|
|
6800
|
+
* verbatim record.
|
|
6801
|
+
*/
|
|
6802
|
+
const sliceWholeCodePoints = (value, max) => {
|
|
6803
|
+
if (value.length <= max) {
|
|
6804
|
+
return value;
|
|
6805
|
+
}
|
|
6806
|
+
const code = value.charCodeAt(max - 1);
|
|
6807
|
+
// High surrogate at the boundary: its pair would be cut, so drop it too.
|
|
6808
|
+
const end = code >= 0xd800 && code <= 0xdbff ? max - 1 : max;
|
|
6809
|
+
return value.slice(0, end);
|
|
6810
|
+
};
|
|
6811
|
+
|
|
6812
|
+
/**
|
|
6813
|
+
* Render an extracted value.
|
|
6814
|
+
*
|
|
6815
|
+
* `literal` is set for code-block values (DNS TXT records and similar). Those come from
|
|
6816
|
+
* the inspected domain's own zone, NOT from our translations, so they are attacker-
|
|
6817
|
+
* controlled and must never go through `parseGlossaryMarkdown` — its documented trust
|
|
6818
|
+
* model assumes source-controlled input, and it turns `[text](https://...)` into a real
|
|
6819
|
+
* anchor. See the adversarial tests in `SignalCardExtractedItems.test.tsx`.
|
|
6820
|
+
*/
|
|
6821
|
+
const renderExtractedValue = function (value, keyPrefix) {
|
|
6822
|
+
let literal = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
|
|
6823
|
+
let budget = arguments.length > 3 ? arguments[3] : undefined;
|
|
6646
6824
|
const nodes = [];
|
|
6825
|
+
// A local budget when none is shared, so non-card callers still get a bound.
|
|
6826
|
+
const b = budget !== null && budget !== void 0 ? budget : createLiteralBudget();
|
|
6647
6827
|
const process = (input, prefix) => {
|
|
6648
6828
|
if (input === null || input === undefined) {
|
|
6649
6829
|
return;
|
|
6650
6830
|
}
|
|
6651
6831
|
if (Array.isArray(input)) {
|
|
6652
|
-
|
|
6832
|
+
for (const entry of input) {
|
|
6833
|
+
// Stop traversing once nothing more can be emitted: continuing would walk the whole
|
|
6834
|
+
// attacker-controlled array to render nothing.
|
|
6835
|
+
if (literal && (budgetExhausted(b) || b.entries <= 0)) {
|
|
6836
|
+
b.hiddenUncounted = true;
|
|
6837
|
+
return;
|
|
6838
|
+
}
|
|
6839
|
+
if (literal) {
|
|
6840
|
+
b.entries -= 1;
|
|
6841
|
+
}
|
|
6842
|
+
process(entry, `${prefix}-${nodes.length}`);
|
|
6843
|
+
}
|
|
6653
6844
|
return;
|
|
6654
6845
|
}
|
|
6655
6846
|
if (typeof input === 'string') {
|
|
6656
|
-
const trimmed = input.trim();
|
|
6657
|
-
if (!trimmed) {
|
|
6847
|
+
const trimmed = literal ? undefined : input.trim();
|
|
6848
|
+
if (!literal && !trimmed) {
|
|
6849
|
+
return;
|
|
6850
|
+
}
|
|
6851
|
+
|
|
6852
|
+
// DS-115: in literal (code-block) mode a single string leaf may itself carry multiple
|
|
6853
|
+
// records separated by newlines — and so may a string nested inside an array, e.g.
|
|
6854
|
+
// `['record A\nrecord B']`. Split every string leaf, not just a top-level one, so each
|
|
6855
|
+
// record gets its own <p> (and so its own CodeBlockLine) rather than flowing together.
|
|
6856
|
+
if (literal) {
|
|
6857
|
+
// Every line becomes a React element and a styled `CodeBlockLine`, and the values are
|
|
6858
|
+
// attacker-controlled: DNS TXT records read from the domain under inspection.
|
|
6859
|
+
// Upstream deliberately does NOT bound the card path (only the context/LLM path is
|
|
6860
|
+
// capped) because a display cap that dropped a whole name could make the
|
|
6861
|
+
// multiple-records predicate deny a real conflict. That is right upstream, and it
|
|
6862
|
+
// makes the RENDER side the place to bound node count and byte size.
|
|
6863
|
+
//
|
|
6864
|
+
// Scanned with an index-bounded loop rather than `split().map().filter().slice()`:
|
|
6865
|
+
// the chained form materializes every line of an adversarial value before discarding
|
|
6866
|
+
// almost all of it, so the work is unbounded even though the output is not. The
|
|
6867
|
+
// string is never `trim()`ed as a whole either — that copies the entire input.
|
|
6868
|
+
let cursor = 0;
|
|
6869
|
+
while (cursor < input.length && !budgetExhausted(b)) {
|
|
6870
|
+
// Bound the scan for one line's end by what could still be rendered, so a single
|
|
6871
|
+
// 10-million-character line is never fully walked or normalized.
|
|
6872
|
+
const scanLimit = Math.min(input.length, cursor + MAX_LITERAL_LINE_CHARS + 1);
|
|
6873
|
+
let next = cursor;
|
|
6874
|
+
while (next < scanLimit && input[next] !== '\n' && input[next] !== '\r') {
|
|
6875
|
+
next += 1;
|
|
6876
|
+
}
|
|
6877
|
+
const overlong = next === scanLimit && next < input.length && input[next] !== '\n' && input[next] !== '\r';
|
|
6878
|
+
const raw = input.slice(cursor, next);
|
|
6879
|
+
const line = raw.replace(/\s+/g, ' ').trim();
|
|
6880
|
+
if (overlong) {
|
|
6881
|
+
// Skip this line’s untruncated remainder WITHOUT materializing it — and BOUND the skip.
|
|
6882
|
+
// Walking to the next newline is still O(input): a 40-million-character line with no
|
|
6883
|
+
// newline took ~164ms against ~97ms for 20M, so the work was linear in attacker input
|
|
6884
|
+
// even though the OUTPUT was bounded. Give up after a bounded probe and report the
|
|
6885
|
+
// rest as uninspected rather than scanning it.
|
|
6886
|
+
const skipLimit = cursor + MAX_LITERAL_LINE_CHARS;
|
|
6887
|
+
while (cursor < input.length && cursor < skipLimit && input[cursor] !== '\n' && input[cursor] !== '\r') {
|
|
6888
|
+
cursor += 1;
|
|
6889
|
+
}
|
|
6890
|
+
if (cursor < input.length && input[cursor] !== '\n' && input[cursor] !== '\r') {
|
|
6891
|
+
// The skip budget ran out before this line ended. Record that the withheld
|
|
6892
|
+
// figure is a floor, then stop consuming budget so the reporting block below
|
|
6893
|
+
// still runs from this cursor — an early `break` here escaped the loop and
|
|
6894
|
+
// skipped reporting entirely, leaving NO notice at all (caught by test).
|
|
6895
|
+
b.hiddenUncounted = true;
|
|
6896
|
+
b.lines = 0;
|
|
6897
|
+
}
|
|
6898
|
+
} else {
|
|
6899
|
+
cursor = next;
|
|
6900
|
+
}
|
|
6901
|
+
|
|
6902
|
+
// Advance past the whole run of newline characters, matching the previous
|
|
6903
|
+
// `/[\r\n]+/` split so a CRLF pair does not emit a blank line between records.
|
|
6904
|
+
while (cursor < input.length && (input[cursor] === '\n' || input[cursor] === '\r')) {
|
|
6905
|
+
cursor += 1;
|
|
6906
|
+
}
|
|
6907
|
+
if (!line) {
|
|
6908
|
+
continue;
|
|
6909
|
+
}
|
|
6910
|
+
const allowed = Math.min(MAX_LITERAL_LINE_CHARS, b.chars);
|
|
6911
|
+
const shown = sliceWholeCodePoints(line, allowed);
|
|
6912
|
+
if (shown.length < line.length || overlong) {
|
|
6913
|
+
b.shortenedLines = true;
|
|
6914
|
+
}
|
|
6915
|
+
nodes.push( /*#__PURE__*/React__default.createElement("p", {
|
|
6916
|
+
key: `${prefix}-text-${nodes.length}`
|
|
6917
|
+
}, shown));
|
|
6918
|
+
b.lines -= 1;
|
|
6919
|
+
b.chars -= shown.length;
|
|
6920
|
+
}
|
|
6921
|
+
|
|
6922
|
+
// Report — never silently drop. This card exists to tell someone which records to
|
|
6923
|
+
// remove, and a hidden remainder invites them to believe they are finished.
|
|
6924
|
+
//
|
|
6925
|
+
// The count is deliberately a LOWER BOUND: an exact figure would mean scanning every
|
|
6926
|
+
// withheld character of an attacker-controlled value. `hiddenUncounted` is what makes
|
|
6927
|
+
// the copy honest about that ("at least N"), so it MUST be set whenever the scan
|
|
6928
|
+
// stopped early. The previous version counted SEPARATORS rather than segments and
|
|
6929
|
+
// could stop on its own budget while still reporting an exact-sounding figure, so 55
|
|
6930
|
+
// records reported 4 withheld when 5 were (verified).
|
|
6931
|
+
if (cursor < input.length) {
|
|
6932
|
+
let hidden = 0;
|
|
6933
|
+
let scan = cursor;
|
|
6934
|
+
// Count SEGMENTS: a segment is a run of non-newline characters.
|
|
6935
|
+
//
|
|
6936
|
+
// BOTH loops are bounded. The segment count is capped by the line budget, and each
|
|
6937
|
+
// segment's own walk is capped by `MAX_LITERAL_LINE_CHARS` — without that inner cap a
|
|
6938
|
+
// remainder holding one very long newline-free line walked the whole string (20M
|
|
6939
|
+
// chars ≈ 42ms, linear in attacker input), which silently undid the "bound WORK, not
|
|
6940
|
+
// only output" guarantee the skip loop above exists to provide.
|
|
6941
|
+
while (scan < input.length && hidden < MAX_LITERAL_LINES) {
|
|
6942
|
+
while (scan < input.length && (input[scan] === '\n' || input[scan] === '\r')) {
|
|
6943
|
+
scan += 1;
|
|
6944
|
+
}
|
|
6945
|
+
if (scan >= input.length) {
|
|
6946
|
+
break;
|
|
6947
|
+
}
|
|
6948
|
+
hidden += 1;
|
|
6949
|
+
const segmentLimit = scan + MAX_LITERAL_LINE_CHARS;
|
|
6950
|
+
while (scan < input.length && scan < segmentLimit && input[scan] !== '\n' && input[scan] !== '\r') {
|
|
6951
|
+
scan += 1;
|
|
6952
|
+
}
|
|
6953
|
+
if (scan < input.length && input[scan] !== '\n' && input[scan] !== '\r') {
|
|
6954
|
+
// Gave up mid-segment, so anything past here is uncounted.
|
|
6955
|
+
b.hiddenUncounted = true;
|
|
6956
|
+
break;
|
|
6957
|
+
}
|
|
6958
|
+
}
|
|
6959
|
+
// Anything left unscanned means the figure is a floor, not a total.
|
|
6960
|
+
if (scan < input.length) {
|
|
6961
|
+
b.hiddenUncounted = true;
|
|
6962
|
+
}
|
|
6963
|
+
b.hiddenLines += hidden;
|
|
6964
|
+
}
|
|
6658
6965
|
return;
|
|
6659
6966
|
}
|
|
6660
6967
|
nodes.push( /*#__PURE__*/React__default.createElement("p", {
|
|
@@ -6664,11 +6971,37 @@ const renderExtractedValue = (value, keyPrefix) => {
|
|
|
6664
6971
|
}
|
|
6665
6972
|
if ( /*#__PURE__*/React__default.isValidElement(input)) {
|
|
6666
6973
|
var _input$key;
|
|
6974
|
+
// Charged against the budget too: element leaves are nodes like any other, and an
|
|
6975
|
+
// uncharged leaf type is an escape hatch — 5,000 object leaves previously rendered
|
|
6976
|
+
// 5,000 lines with no notice.
|
|
6977
|
+
if (literal) {
|
|
6978
|
+
if (budgetExhausted(b)) {
|
|
6979
|
+
b.hiddenLines += 1;
|
|
6980
|
+
return;
|
|
6981
|
+
}
|
|
6982
|
+
b.lines -= 1;
|
|
6983
|
+
}
|
|
6667
6984
|
nodes.push( /*#__PURE__*/React__default.cloneElement(input, {
|
|
6668
6985
|
key: (_input$key = input.key) !== null && _input$key !== void 0 ? _input$key : `${prefix}-element`
|
|
6669
6986
|
}));
|
|
6670
6987
|
return;
|
|
6671
6988
|
}
|
|
6989
|
+
|
|
6990
|
+
// Numbers, booleans, objects. Stringified, so they are bounded and charged exactly like
|
|
6991
|
+
// string leaves rather than bypassing the budget.
|
|
6992
|
+
if (literal) {
|
|
6993
|
+
if (budgetExhausted(b)) {
|
|
6994
|
+
b.hiddenLines += 1;
|
|
6995
|
+
return;
|
|
6996
|
+
}
|
|
6997
|
+
const text = sliceWholeCodePoints(String(input), Math.min(MAX_LITERAL_LINE_CHARS, b.chars));
|
|
6998
|
+
b.lines -= 1;
|
|
6999
|
+
b.chars -= text.length;
|
|
7000
|
+
nodes.push( /*#__PURE__*/React__default.createElement("p", {
|
|
7001
|
+
key: `${prefix}-fallback`
|
|
7002
|
+
}, text));
|
|
7003
|
+
return;
|
|
7004
|
+
}
|
|
6672
7005
|
nodes.push( /*#__PURE__*/React__default.createElement("p", {
|
|
6673
7006
|
key: `${prefix}-fallback`
|
|
6674
7007
|
}, String(input)));
|
|
@@ -6676,6 +7009,65 @@ const renderExtractedValue = (value, keyPrefix) => {
|
|
|
6676
7009
|
process(value, keyPrefix);
|
|
6677
7010
|
return nodes;
|
|
6678
7011
|
};
|
|
7012
|
+
|
|
7013
|
+
/**
|
|
7014
|
+
* Wrap already-literal, newline-split nodes in a `CodeBlock`, one `CodeBlockLine` per record.
|
|
7015
|
+
*
|
|
7016
|
+
* Shared by every code-block path so they cannot drift apart: `CodeBlock` sets
|
|
7017
|
+
* `white-space: normal`, so without one element per line multiple records flow together.
|
|
7018
|
+
*/
|
|
7019
|
+
const renderCodeBlock = nodes => /*#__PURE__*/React__default.createElement(CodeBlock, null, nodes.map((node, idx) => {
|
|
7020
|
+
if ( /*#__PURE__*/React__default.isValidElement(node) && node.type === 'p') {
|
|
7021
|
+
var _node$key;
|
|
7022
|
+
return /*#__PURE__*/React__default.createElement(CodeBlockLine, {
|
|
7023
|
+
key: (_node$key = node.key) !== null && _node$key !== void 0 ? _node$key : `code-line-${idx}`
|
|
7024
|
+
}, node.props.children);
|
|
7025
|
+
}
|
|
7026
|
+
return node;
|
|
7027
|
+
}));
|
|
7028
|
+
|
|
7029
|
+
/**
|
|
7030
|
+
* Notice that the renderer withheld part of a literal value.
|
|
7031
|
+
*
|
|
7032
|
+
* The caps above are initial-render SAFETY limits, not a completeness guarantee: DNS defines
|
|
7033
|
+
* no record-count limit for a name, a TXT RR may concatenate several 255-octet strings, and
|
|
7034
|
+
* under RFC 9989 §4.10 every competing record is discarded — so dozens of short records are
|
|
7035
|
+
* protocol-plausible and each withheld value is one the operator still has to delete.
|
|
7036
|
+
*
|
|
7037
|
+
* Reporting is therefore the minimum: truncating silently would tell someone the list is
|
|
7038
|
+
* complete when it is not. A "copy all published records" action was built and REVERTED (see
|
|
7039
|
+
* CHANGELOG) — it could not honour its own promise, so full-evidence access is tracked
|
|
7040
|
+
* separately rather than half-delivered here.
|
|
7041
|
+
*
|
|
7042
|
+
* Rendered OUTSIDE the `CodeBlock` and as its own `LiteralTruncationNotice`, for a security
|
|
7043
|
+
* reason rather than a stylistic one: inside the block it was structurally and visually
|
|
7044
|
+
* identical to a record, so a zone publishing the literal text `[… 3 more records not shown]`
|
|
7045
|
+
* produced an indistinguishable line (verified). Zone content and renderer commentary must
|
|
7046
|
+
* not share a presentation. `data-testid` is stable so tests can assert the notice
|
|
7047
|
+
* specifically, rather than matching prose an attacker could also publish.
|
|
7048
|
+
*/
|
|
7049
|
+
const renderTruncationNotice = (budget, t) => {
|
|
7050
|
+
const parts = [];
|
|
7051
|
+
if (budget.hiddenLines > 0) {
|
|
7052
|
+
parts.push(t(budget.hiddenUncounted ? 'card.extracted.truncated-records-atleast' : 'card.extracted.truncated-records', {
|
|
7053
|
+
count: budget.hiddenLines
|
|
7054
|
+
}));
|
|
7055
|
+
} else if (budget.hiddenUncounted) {
|
|
7056
|
+
parts.push(t('card.extracted.truncated-records-uninspected'));
|
|
7057
|
+
}
|
|
7058
|
+
|
|
7059
|
+
// Reported ALONGSIDE the hidden count, not instead of it: a payload that both shortened
|
|
7060
|
+
// every visible record and hid further ones must not announce only one of the two.
|
|
7061
|
+
if (budget.shortenedLines) {
|
|
7062
|
+
parts.push(t('card.extracted.truncated-record-length'));
|
|
7063
|
+
}
|
|
7064
|
+
if (!parts.length) {
|
|
7065
|
+
return null;
|
|
7066
|
+
}
|
|
7067
|
+
return /*#__PURE__*/React__default.createElement(LiteralTruncationNotice, {
|
|
7068
|
+
"data-testid": "extracted-truncation-notice"
|
|
7069
|
+
}, parts.join(' '));
|
|
7070
|
+
};
|
|
6679
7071
|
const ExtractedItem = _ref2 => {
|
|
6680
7072
|
let {
|
|
6681
7073
|
type,
|
|
@@ -6684,7 +7076,8 @@ const ExtractedItem = _ref2 => {
|
|
|
6684
7076
|
text,
|
|
6685
7077
|
inline = true,
|
|
6686
7078
|
useCodeBlock = false,
|
|
6687
|
-
extractionBox
|
|
7079
|
+
extractionBox,
|
|
7080
|
+
literalBudget
|
|
6688
7081
|
} = _ref2;
|
|
6689
7082
|
// Get translated caption for extraction box
|
|
6690
7083
|
const {
|
|
@@ -6695,29 +7088,25 @@ const ExtractedItem = _ref2 => {
|
|
|
6695
7088
|
// Parse label if it's a string with glossary markdown
|
|
6696
7089
|
const parsedLabel = typeof label === 'string' ? parseGlossaryMarkdown(label) : label;
|
|
6697
7090
|
|
|
6698
|
-
//
|
|
6699
|
-
|
|
6700
|
-
|
|
7091
|
+
// DS-115: a code-block value may legitimately be MULTIPLE records (e.g. a domain that
|
|
7092
|
+
// publishes two competing `_dmarc` TXT records). Collapsing newlines into spaces ran them
|
|
7093
|
+
// together into one flowed line with no visible boundary, which is precisely the condition
|
|
7094
|
+
// the card exists to report. `renderExtractedValue`'s literal mode splits every string leaf
|
|
7095
|
+
// on newlines (including strings nested in arrays) so each record renders on its own line.
|
|
7096
|
+
const budget = literalBudget !== null && literalBudget !== void 0 ? literalBudget : createLiteralBudget();
|
|
7097
|
+
const renderedText = renderExtractedValue(text, 'extracted-item-text', useCodeBlock, budget);
|
|
6701
7098
|
const icon = getIcon({
|
|
6702
7099
|
type,
|
|
6703
7100
|
theme
|
|
6704
7101
|
});
|
|
6705
7102
|
|
|
6706
|
-
// Wrap in CodeBlock if needed
|
|
6707
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
const children = node.props.children;
|
|
6711
|
-
|
|
6712
|
-
// If children is a string, strip ALL newlines aggressively
|
|
6713
|
-
const cleanedChildren = typeof children === 'string' ? children.replace(/[\r\n\t]+/g, ' ').replace(/\s+/g, ' ').trim() : children;
|
|
6714
|
-
return cleanedChildren;
|
|
6715
|
-
}
|
|
6716
|
-
return node;
|
|
6717
|
-
}))) : renderedText;
|
|
7103
|
+
// Wrap in CodeBlock if needed. The truncation notice sits OUTSIDE the block — see
|
|
7104
|
+
// `renderTruncationNotice` on why it must never be a `CodeBlockLine`.
|
|
7105
|
+
const displayText = useCodeBlock ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, renderCodeBlock(renderedText), renderTruncationNotice(budget, t)) : renderedText;
|
|
7106
|
+
const hasContent = renderedText.length > 0;
|
|
6718
7107
|
const content = /*#__PURE__*/React__default.createElement(SignalCardInfoBox$1, null, /*#__PURE__*/React__default.createElement(InfoItemRow, null, icon ? /*#__PURE__*/React__default.createElement(InfoItemIcon, null, icon) : null, /*#__PURE__*/React__default.createElement(InfoItemContent, null, /*#__PURE__*/React__default.createElement(InfoItemLabel, {
|
|
6719
7108
|
$inline: true
|
|
6720
|
-
}, parsedLabel), inline &&
|
|
7109
|
+
}, parsedLabel), inline && hasContent ? displayText : null)), !inline && hasContent ? /*#__PURE__*/React__default.createElement(InfoItemSubtexts, null, displayText) : null);
|
|
6721
7110
|
|
|
6722
7111
|
// Wrap in extraction box if specified
|
|
6723
7112
|
if (extractionBox) {
|
|
@@ -6727,9 +7116,12 @@ const ExtractedItem = _ref2 => {
|
|
|
6727
7116
|
}
|
|
6728
7117
|
return content;
|
|
6729
7118
|
};
|
|
6730
|
-
function renderGridItem(item, idx, t) {
|
|
7119
|
+
function renderGridItem(item, idx, t, budget) {
|
|
6731
7120
|
const parsedLabel = typeof item.label === 'string' ? parseGlossaryMarkdown(item.label) : item.label;
|
|
6732
|
-
|
|
7121
|
+
// DS-115: code-block values are attacker-controlled DNS data, so render them literally —
|
|
7122
|
+
// same trust model as `ExtractedItem`. Without this the grid path fed raw TXT records
|
|
7123
|
+
// through `parseGlossaryMarkdown`, which turns `[text](https://...)` into a real anchor.
|
|
7124
|
+
const renderedText = renderExtractedValue(item.text, `grid-item-${idx}`, !!item.useCodeBlock, budget);
|
|
6733
7125
|
const icon = getIcon({
|
|
6734
7126
|
type: item.type,
|
|
6735
7127
|
theme: item.theme
|
|
@@ -6749,7 +7141,7 @@ function renderGridItem(item, idx, t) {
|
|
|
6749
7141
|
display: 'inline-flex'
|
|
6750
7142
|
}
|
|
6751
7143
|
}, icon))) : null;
|
|
6752
|
-
const displayValue = item.useCodeBlock ? /*#__PURE__*/React__default.createElement(
|
|
7144
|
+
const displayValue = item.useCodeBlock ? /*#__PURE__*/React__default.createElement(React__default.Fragment, null, renderCodeBlock(renderedText), renderTruncationNotice(budget, t)) : /*#__PURE__*/React__default.createElement(GridValue, null, renderedText, alignmentIcon);
|
|
6753
7145
|
return /*#__PURE__*/React__default.createElement(React__default.Fragment, {
|
|
6754
7146
|
key: idx
|
|
6755
7147
|
}, /*#__PURE__*/React__default.createElement(GridLabel, null, icon && !item.tooltipKey ? /*#__PURE__*/React__default.createElement("span", {
|
|
@@ -6768,6 +7160,15 @@ const ExtractedCategoryContent = _ref3 => {
|
|
|
6768
7160
|
const {
|
|
6769
7161
|
t
|
|
6770
7162
|
} = useSignalCardTranslation('investigate');
|
|
7163
|
+
// ONE budget per category render: upstream emits one item per candidate group, so a
|
|
7164
|
+
// per-item budget let 20 groups render 20 x 50 lines with 20 separate notices.
|
|
7165
|
+
//
|
|
7166
|
+
// The notice is emitted per ITEM rather than once per category. Hoisting it was attempted
|
|
7167
|
+
// and reverted: the budget is mutated while the ITEMS render, and a parent cannot observe
|
|
7168
|
+
// that — `<Body/>` is only an element when the parent returns, so the notice read a pristine
|
|
7169
|
+
// budget and never appeared. Doing it properly means precomputing each category's preview
|
|
7170
|
+
// and truncation state before rendering, which is tracked separately (see CHANGELOG).
|
|
7171
|
+
const literalBudget = createLiteralBudget();
|
|
6771
7172
|
const useGridLayout = category.categoryType === 'dmarc-domain' || category.categoryType === 'dmarc-policy' || category.categoryType === 'dkim-details';
|
|
6772
7173
|
if (useGridLayout) {
|
|
6773
7174
|
var _category$items, _category$items2;
|
|
@@ -6799,12 +7200,13 @@ const ExtractedCategoryContent = _ref3 => {
|
|
|
6799
7200
|
text: text,
|
|
6800
7201
|
inline: inline,
|
|
6801
7202
|
useCodeBlock: useCodeBlock,
|
|
6802
|
-
extractionBox: undefined
|
|
7203
|
+
extractionBox: undefined,
|
|
7204
|
+
literalBudget: literalBudget
|
|
6803
7205
|
});
|
|
6804
7206
|
}), /*#__PURE__*/React__default.createElement(InfoItemsGrid, {
|
|
6805
7207
|
$hasNonInlineItems: hasNonInlineItems,
|
|
6806
7208
|
$insideCollapsible: true
|
|
6807
|
-
}, (_category$items5 = category.items) === null || _category$items5 === void 0 ? void 0 : _category$items5.filter(item => item.inline !== false).map((item, idx) => renderGridItem(item, idx, t))));
|
|
7209
|
+
}, (_category$items5 = category.items) === null || _category$items5 === void 0 ? void 0 : _category$items5.filter(item => item.inline !== false).map((item, idx) => renderGridItem(item, idx, t, literalBudget))));
|
|
6808
7210
|
} else if (hasNonInlineItems) {
|
|
6809
7211
|
var _category$items6, _category$items6$find, _category$items6$find2, _category$items7;
|
|
6810
7212
|
// Only TXT records present
|
|
@@ -6830,7 +7232,8 @@ const ExtractedCategoryContent = _ref3 => {
|
|
|
6830
7232
|
text: text,
|
|
6831
7233
|
inline: inline,
|
|
6832
7234
|
useCodeBlock: useCodeBlock,
|
|
6833
|
-
extractionBox: undefined
|
|
7235
|
+
extractionBox: undefined,
|
|
7236
|
+
literalBudget: literalBudget
|
|
6834
7237
|
});
|
|
6835
7238
|
}));
|
|
6836
7239
|
} else {
|
|
@@ -6841,7 +7244,7 @@ const ExtractedCategoryContent = _ref3 => {
|
|
|
6841
7244
|
}, /*#__PURE__*/React__default.createElement(InfoItemsGrid, {
|
|
6842
7245
|
$hasNonInlineItems: false,
|
|
6843
7246
|
$insideCollapsible: true
|
|
6844
|
-
}, (_category$items8 = category.items) === null || _category$items8 === void 0 ? void 0 : _category$items8.filter(item => item.inline !== false).map((item, idx) => renderGridItem(item, idx, t))));
|
|
7247
|
+
}, (_category$items8 = category.items) === null || _category$items8 === void 0 ? void 0 : _category$items8.filter(item => item.inline !== false).map((item, idx) => renderGridItem(item, idx, t, literalBudget))));
|
|
6845
7248
|
}
|
|
6846
7249
|
}
|
|
6847
7250
|
|
|
@@ -6867,7 +7270,8 @@ const ExtractedCategoryContent = _ref3 => {
|
|
|
6867
7270
|
text: text,
|
|
6868
7271
|
inline: inline,
|
|
6869
7272
|
useCodeBlock: useCodeBlock,
|
|
6870
|
-
extractionBox: undefined
|
|
7273
|
+
extractionBox: undefined,
|
|
7274
|
+
literalBudget: literalBudget
|
|
6871
7275
|
});
|
|
6872
7276
|
})));
|
|
6873
7277
|
};
|