@khorsheed/dsh-reader 0.2.0 → 0.3.0

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.
@@ -103,6 +103,17 @@ const _khorsheed_dsh_reader_reader_fetchEntryBody_result$schema = () => (_khorsh
103
103
  'truncated': z.boolean().optional(),
104
104
  'error': z.string().optional(),
105
105
  }))
106
+ let _khorsheed_dsh_reader_reader_fetchImage_parameter_0$schema$value
107
+ const _khorsheed_dsh_reader_reader_fetchImage_parameter_0$schema = () => (_khorsheed_dsh_reader_reader_fetchImage_parameter_0$schema$value ??= z.object({
108
+ 'url': z.string(),
109
+ }))
110
+ let _khorsheed_dsh_reader_reader_fetchImage_result$schema$value
111
+ const _khorsheed_dsh_reader_reader_fetchImage_result$schema = () => (_khorsheed_dsh_reader_reader_fetchImage_result$schema$value ??= z.object({
112
+ 'url': z.string().readonly(),
113
+ 'mime': z.string().readonly().optional(),
114
+ 'base64': z.string().readonly().optional(),
115
+ 'error': z.string().readonly().optional(),
116
+ }))
106
117
  let _khorsheed_dsh_reader_reader_getBodies_parameter_0$schema$value
107
118
  const _khorsheed_dsh_reader_reader_getBodies_parameter_0$schema = () => (_khorsheed_dsh_reader_reader_getBodies_parameter_0$schema$value ??= z.object({
108
119
  'ids': z.array(z.string()),
@@ -396,7 +407,7 @@ export const TYPERT_REMOTE = {
396
407
  create: _khorsheed_dsh_reader_reader_addSource_result$schema,
397
408
  schema: _khorsheed_dsh_reader_reader_addSource_result$schema(),
398
409
  },
399
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":71,"column":3},
410
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":72,"column":3},
400
411
  },
401
412
  {
402
413
  id: '@khorsheed/dsh-reader#reader/capabilities',
@@ -412,7 +423,7 @@ export const TYPERT_REMOTE = {
412
423
  create: _khorsheed_dsh_reader_reader_capabilities_result$schema,
413
424
  schema: _khorsheed_dsh_reader_reader_capabilities_result$schema(),
414
425
  },
415
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":59,"column":3},
426
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":60,"column":3},
416
427
  },
417
428
  {
418
429
  id: '@khorsheed/dsh-reader#reader/clearRecent',
@@ -428,7 +439,7 @@ export const TYPERT_REMOTE = {
428
439
  create: _khorsheed_dsh_reader_reader_clearRecent_result$schema,
429
440
  schema: _khorsheed_dsh_reader_reader_clearRecent_result$schema(),
430
441
  },
431
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":274,"column":3},
442
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":285,"column":3},
432
443
  },
433
444
  {
434
445
  id: '@khorsheed/dsh-reader#reader/clearTranslations',
@@ -444,7 +455,7 @@ export const TYPERT_REMOTE = {
444
455
  create: _khorsheed_dsh_reader_reader_clearTranslations_result$schema,
445
456
  schema: _khorsheed_dsh_reader_reader_clearTranslations_result$schema(),
446
457
  },
447
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":198,"column":3},
458
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":209,"column":3},
448
459
  },
449
460
  {
450
461
  id: '@khorsheed/dsh-reader#reader/createTag',
@@ -471,7 +482,7 @@ export const TYPERT_REMOTE = {
471
482
  create: _khorsheed_dsh_reader_reader_createTag_result$schema,
472
483
  schema: _khorsheed_dsh_reader_reader_createTag_result$schema(),
473
484
  },
474
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":210,"column":3},
485
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":221,"column":3},
475
486
  },
476
487
  {
477
488
  id: '@khorsheed/dsh-reader#reader/deleteTag',
@@ -498,7 +509,7 @@ export const TYPERT_REMOTE = {
498
509
  create: _khorsheed_dsh_reader_reader_deleteTag_result$schema,
499
510
  schema: _khorsheed_dsh_reader_reader_deleteTag_result$schema(),
500
511
  },
501
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":222,"column":3},
512
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":233,"column":3},
502
513
  },
503
514
  {
504
515
  id: '@khorsheed/dsh-reader#reader/entryFetchStates',
@@ -525,7 +536,7 @@ export const TYPERT_REMOTE = {
525
536
  create: _khorsheed_dsh_reader_reader_entryFetchStates_result$schema,
526
537
  schema: _khorsheed_dsh_reader_reader_entryFetchStates_result$schema(),
527
538
  },
528
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":145,"column":3},
539
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":156,"column":3},
529
540
  },
530
541
  {
531
542
  id: '@khorsheed/dsh-reader#reader/entryTags',
@@ -552,7 +563,7 @@ export const TYPERT_REMOTE = {
552
563
  create: _khorsheed_dsh_reader_reader_entryTags_result$schema,
553
564
  schema: _khorsheed_dsh_reader_reader_entryTags_result$schema(),
554
565
  },
555
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":234,"column":3},
566
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":245,"column":3},
556
567
  },
557
568
  {
558
569
  id: '@khorsheed/dsh-reader#reader/fetchEntryBody',
@@ -579,7 +590,34 @@ export const TYPERT_REMOTE = {
579
590
  create: _khorsheed_dsh_reader_reader_fetchEntryBody_result$schema,
580
591
  schema: _khorsheed_dsh_reader_reader_fetchEntryBody_result$schema(),
581
592
  },
582
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":115,"column":3},
593
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":116,"column":3},
594
+ },
595
+ {
596
+ id: '@khorsheed/dsh-reader#reader/fetchImage',
597
+ service: 'readerRemote',
598
+ namespace: 'reader',
599
+ method: 'fetchImage',
600
+ invocation: { kind: 'direct' },
601
+ parameters: [
602
+ {
603
+ name: 'request',
604
+ wire: 'request',
605
+ source: 'json',
606
+ codec: {
607
+ mode: 'strict',
608
+ typeSymbol: '@khorsheed/dsh-reader#reader/fetchImage:request',
609
+ create: _khorsheed_dsh_reader_reader_fetchImage_parameter_0$schema,
610
+ schema: _khorsheed_dsh_reader_reader_fetchImage_parameter_0$schema(),
611
+ },
612
+ },
613
+ ],
614
+ result: {
615
+ mode: 'strict',
616
+ typeSymbol: '@khorsheed/dsh-reader/types#ReaderImageResult',
617
+ create: _khorsheed_dsh_reader_reader_fetchImage_result$schema,
618
+ schema: _khorsheed_dsh_reader_reader_fetchImage_result$schema(),
619
+ },
620
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":133,"column":3},
583
621
  },
584
622
  {
585
623
  id: '@khorsheed/dsh-reader#reader/getBodies',
@@ -606,7 +644,7 @@ export const TYPERT_REMOTE = {
606
644
  create: _khorsheed_dsh_reader_reader_getBodies_result$schema,
607
645
  schema: _khorsheed_dsh_reader_reader_getBodies_result$schema(),
608
646
  },
609
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":101,"column":9},
647
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":102,"column":9},
610
648
  },
611
649
  {
612
650
  id: '@khorsheed/dsh-reader#reader/getCachePolicy',
@@ -622,7 +660,7 @@ export const TYPERT_REMOTE = {
622
660
  create: _khorsheed_dsh_reader_reader_getCachePolicy_result$schema,
623
661
  schema: _khorsheed_dsh_reader_reader_getCachePolicy_result$schema(),
624
662
  },
625
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":240,"column":3},
663
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":251,"column":3},
626
664
  },
627
665
  {
628
666
  id: '@khorsheed/dsh-reader#reader/getEntryBody',
@@ -649,7 +687,7 @@ export const TYPERT_REMOTE = {
649
687
  create: _khorsheed_dsh_reader_reader_getEntryBody_result$schema,
650
688
  schema: _khorsheed_dsh_reader_reader_getEntryBody_result$schema(),
651
689
  },
652
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":109,"column":3},
690
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":110,"column":3},
653
691
  },
654
692
  {
655
693
  id: '@khorsheed/dsh-reader#reader/getEntryTranslation',
@@ -676,7 +714,7 @@ export const TYPERT_REMOTE = {
676
714
  create: _khorsheed_dsh_reader_reader_getEntryTranslation_result$schema,
677
715
  schema: _khorsheed_dsh_reader_reader_getEntryTranslation_result$schema(),
678
716
  },
679
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":168,"column":3},
717
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":179,"column":3},
680
718
  },
681
719
  {
682
720
  id: '@khorsheed/dsh-reader#reader/getRawBody',
@@ -703,7 +741,7 @@ export const TYPERT_REMOTE = {
703
741
  create: _khorsheed_dsh_reader_reader_getRawBody_result$schema,
704
742
  schema: _khorsheed_dsh_reader_reader_getRawBody_result$schema(),
705
743
  },
706
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":151,"column":3},
744
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":162,"column":3},
707
745
  },
708
746
  {
709
747
  id: '@khorsheed/dsh-reader#reader/getSentenceTranslations',
@@ -730,7 +768,7 @@ export const TYPERT_REMOTE = {
730
768
  create: _khorsheed_dsh_reader_reader_getSentenceTranslations_result$schema,
731
769
  schema: _khorsheed_dsh_reader_reader_getSentenceTranslations_result$schema(),
732
770
  },
733
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":174,"column":3},
771
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":185,"column":3},
734
772
  },
735
773
  {
736
774
  id: '@khorsheed/dsh-reader#reader/getStorageStats',
@@ -746,7 +784,7 @@ export const TYPERT_REMOTE = {
746
784
  create: _khorsheed_dsh_reader_reader_getStorageStats_result$schema,
747
785
  schema: _khorsheed_dsh_reader_reader_getStorageStats_result$schema(),
748
786
  },
749
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":192,"column":3},
787
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":203,"column":3},
750
788
  },
751
789
  {
752
790
  id: '@khorsheed/dsh-reader#reader/listBackfillCandidates',
@@ -773,7 +811,7 @@ export const TYPERT_REMOTE = {
773
811
  create: _khorsheed_dsh_reader_reader_listBackfillCandidates_result$schema,
774
812
  schema: _khorsheed_dsh_reader_reader_listBackfillCandidates_result$schema(),
775
813
  },
776
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":157,"column":3},
814
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":168,"column":3},
777
815
  },
778
816
  {
779
817
  id: '@khorsheed/dsh-reader#reader/listRecent',
@@ -789,7 +827,7 @@ export const TYPERT_REMOTE = {
789
827
  create: _khorsheed_dsh_reader_reader_listRecent_result$schema,
790
828
  schema: _khorsheed_dsh_reader_reader_listRecent_result$schema(),
791
829
  },
792
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":268,"column":3},
830
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":279,"column":3},
793
831
  },
794
832
  {
795
833
  id: '@khorsheed/dsh-reader#reader/listSources',
@@ -805,7 +843,7 @@ export const TYPERT_REMOTE = {
805
843
  create: _khorsheed_dsh_reader_reader_listSources_result$schema,
806
844
  schema: _khorsheed_dsh_reader_reader_listSources_result$schema(),
807
845
  },
808
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":65,"column":9},
846
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":66,"column":9},
809
847
  },
810
848
  {
811
849
  id: '@khorsheed/dsh-reader#reader/listTags',
@@ -821,7 +859,7 @@ export const TYPERT_REMOTE = {
821
859
  create: _khorsheed_dsh_reader_reader_listTags_result$schema,
822
860
  schema: _khorsheed_dsh_reader_reader_listTags_result$schema(),
823
861
  },
824
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":204,"column":3},
862
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":215,"column":3},
825
863
  },
826
864
  {
827
865
  id: '@khorsheed/dsh-reader#reader/pruneTags',
@@ -837,7 +875,7 @@ export const TYPERT_REMOTE = {
837
875
  create: _khorsheed_dsh_reader_reader_pruneTags_result$schema,
838
876
  schema: _khorsheed_dsh_reader_reader_pruneTags_result$schema(),
839
877
  },
840
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":251,"column":3},
878
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":262,"column":3},
841
879
  },
842
880
  {
843
881
  id: '@khorsheed/dsh-reader#reader/quoteToSideChat',
@@ -864,7 +902,7 @@ export const TYPERT_REMOTE = {
864
902
  create: _khorsheed_dsh_reader_reader_quoteToSideChat_result$schema,
865
903
  schema: _khorsheed_dsh_reader_reader_quoteToSideChat_result$schema(),
866
904
  },
867
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":280,"column":3},
905
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":291,"column":3},
868
906
  },
869
907
  {
870
908
  id: '@khorsheed/dsh-reader#reader/recordRead',
@@ -891,7 +929,7 @@ export const TYPERT_REMOTE = {
891
929
  create: _khorsheed_dsh_reader_reader_recordRead_result$schema,
892
930
  schema: _khorsheed_dsh_reader_reader_recordRead_result$schema(),
893
931
  },
894
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":257,"column":3},
932
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":268,"column":3},
895
933
  },
896
934
  {
897
935
  id: '@khorsheed/dsh-reader#reader/refresh',
@@ -918,7 +956,7 @@ export const TYPERT_REMOTE = {
918
956
  create: _khorsheed_dsh_reader_reader_refresh_result$schema,
919
957
  schema: _khorsheed_dsh_reader_reader_refresh_result$schema(),
920
958
  },
921
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":95,"column":9},
959
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":96,"column":9},
922
960
  },
923
961
  {
924
962
  id: '@khorsheed/dsh-reader#reader/rememberSentences',
@@ -945,7 +983,7 @@ export const TYPERT_REMOTE = {
945
983
  create: _khorsheed_dsh_reader_reader_rememberSentences_result$schema,
946
984
  schema: _khorsheed_dsh_reader_reader_rememberSentences_result$schema(),
947
985
  },
948
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":180,"column":3},
986
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":191,"column":3},
949
987
  },
950
988
  {
951
989
  id: '@khorsheed/dsh-reader#reader/removeSource',
@@ -972,7 +1010,7 @@ export const TYPERT_REMOTE = {
972
1010
  create: _khorsheed_dsh_reader_reader_removeSource_result$schema,
973
1011
  schema: _khorsheed_dsh_reader_reader_removeSource_result$schema(),
974
1012
  },
975
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":89,"column":3},
1013
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":90,"column":3},
976
1014
  },
977
1015
  {
978
1016
  id: '@khorsheed/dsh-reader#reader/renameTag',
@@ -999,7 +1037,7 @@ export const TYPERT_REMOTE = {
999
1037
  create: _khorsheed_dsh_reader_reader_renameTag_result$schema,
1000
1038
  schema: _khorsheed_dsh_reader_reader_renameTag_result$schema(),
1001
1039
  },
1002
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":216,"column":3},
1040
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":227,"column":3},
1003
1041
  },
1004
1042
  {
1005
1043
  id: '@khorsheed/dsh-reader#reader/setCachePolicy',
@@ -1026,7 +1064,7 @@ export const TYPERT_REMOTE = {
1026
1064
  create: _khorsheed_dsh_reader_reader_setCachePolicy_result$schema,
1027
1065
  schema: _khorsheed_dsh_reader_reader_setCachePolicy_result$schema(),
1028
1066
  },
1029
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":245,"column":3},
1067
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":256,"column":3},
1030
1068
  },
1031
1069
  {
1032
1070
  id: '@khorsheed/dsh-reader#reader/storeEntryBody',
@@ -1053,7 +1091,7 @@ export const TYPERT_REMOTE = {
1053
1091
  create: _khorsheed_dsh_reader_reader_storeEntryBody_result$schema,
1054
1092
  schema: _khorsheed_dsh_reader_reader_storeEntryBody_result$schema(),
1055
1093
  },
1056
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":128,"column":3},
1094
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":139,"column":3},
1057
1095
  },
1058
1096
  {
1059
1097
  id: '@khorsheed/dsh-reader#reader/tagEntry',
@@ -1080,7 +1118,7 @@ export const TYPERT_REMOTE = {
1080
1118
  create: _khorsheed_dsh_reader_reader_tagEntry_result$schema,
1081
1119
  schema: _khorsheed_dsh_reader_reader_tagEntry_result$schema(),
1082
1120
  },
1083
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":228,"column":3},
1121
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":239,"column":3},
1084
1122
  },
1085
1123
  {
1086
1124
  id: '@khorsheed/dsh-reader#reader/updateSource',
@@ -1107,7 +1145,7 @@ export const TYPERT_REMOTE = {
1107
1145
  create: _khorsheed_dsh_reader_reader_updateSource_result$schema,
1108
1146
  schema: _khorsheed_dsh_reader_reader_updateSource_result$schema(),
1109
1147
  },
1110
- sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":77,"column":3},
1148
+ sourceLocation: {"file":"packages/dsh-reader/src/remote.ts","line":78,"column":3},
1111
1149
  },
1112
1150
  ],
1113
1151
  }
@@ -626,6 +626,8 @@ export function ReaderPane(props) {
626
626
  const [tipDismissed, setTipDismissed] = useState(false);
627
627
  /** The rendered article, so the segmentation can be applied to it. */
628
628
  const articleRef = useRef(null);
629
+ /** Images already rescued through the host, original URL → data URI (this mount's). */
630
+ const rescuedImagesRef = useRef(new Map());
629
631
  /** The detail view's scroller, which is where a reading position lives. */
630
632
  const detailRef = useRef(null);
631
633
  /** The live segmentation, or null while the article is untouched. */
@@ -1550,6 +1552,46 @@ export function ReaderPane(props) {
1550
1552
  return;
1551
1553
  toggleSegment(built, segment, translateClasses);
1552
1554
  }, [translateClasses, translatePhase]);
1555
+ /**
1556
+ * The CORP rescue. The extractor already emits absolute URLs with
1557
+ * `referrerpolicy="no-referrer"`, which covers referer-gated CDNs — but a
1558
+ * response headed `Cross-Origin-Resource-Policy: same-origin` (claude.dev's
1559
+ * `/media/*` figures, measured 2026-09-29) is withheld from this page no
1560
+ * matter what the tag asks for: the request answers 200 and the browser
1561
+ * still refuses to hand the pixels over, leaving the broken-image icon over
1562
+ * the alt text. A no-cors browser fetch cannot read those bytes either; the
1563
+ * host process can, so the failed image is re-pointed at what the host
1564
+ * fetched. Only images that ALREADY failed come here — a healthy image never
1565
+ * pays the proxy — and each `<img>` tries once: the marker doubles as the
1566
+ * loop guard should the data URI itself fail.
1567
+ */
1568
+ const onArticleImageError = useCallback((event) => {
1569
+ const target = event.target;
1570
+ if (!(target instanceof HTMLImageElement))
1571
+ return;
1572
+ if (target.dataset['readerRescued'] !== undefined)
1573
+ return;
1574
+ const url = target.getAttribute('src') ?? '';
1575
+ // Only a remote URL can be rescued; a data: failure is a broken payload.
1576
+ if (!/^https?:\/\//i.test(url))
1577
+ return;
1578
+ target.dataset['readerRescued'] = '1';
1579
+ const known = rescuedImagesRef.current.get(url);
1580
+ if (known !== undefined) {
1581
+ target.src = known;
1582
+ return;
1583
+ }
1584
+ void props.fetchImage(url).then(result => {
1585
+ if (!result.ok)
1586
+ return;
1587
+ const { mime, base64 } = result.value;
1588
+ if (mime === undefined || base64 === undefined)
1589
+ return;
1590
+ const dataUri = `data:${mime};base64,${base64}`;
1591
+ rescuedImagesRef.current.set(url, dataUri);
1592
+ target.src = dataUri;
1593
+ }, () => undefined);
1594
+ }, [props]);
1553
1595
  /* ---------------------------------------------------- the wall's own pass */
1554
1596
  // The wall has no article to segment: a card's unit is a FIELD (title,
1555
1597
  // summary), and the wall must translate lazily — a wall of forty cards is
@@ -2878,7 +2920,10 @@ export function ReaderPane(props) {
2878
2920
  // `translate.ts` decorates THIS subtree in place when the reader
2879
2921
  // asks for a translation (text nodes only), so the click handler
2880
2922
  // is delegation: the sentences it creates are not React's.
2881
- ref: articleRef, onClick: onArticleClick, onMouseOver: onArticleHover, onMouseOut: onArticleHover, className: `${css.article} ${isCjk(articleHtml) ? css.articleZh : css.articleEn}`, dangerouslySetInnerHTML: { __html: articleHtml } })), (articleTruncated || openEntry.truncated === true) && (_jsxs("p", { className: css.incomplete, children: [t('detail.incomplete'), " \u2014", ' ', _jsx("button", { type: "button", className: css.incompleteLink, onClick: () => { if (openEntry.link !== undefined)
2923
+ ref: articleRef, onClick: onArticleClick, onMouseOver: onArticleHover, onMouseOut: onArticleHover,
2924
+ // Image error events do not bubble; the capture phase is the
2925
+ // only way one handler sees every failed figure in the body.
2926
+ onErrorCapture: onArticleImageError, className: `${css.article} ${isCjk(articleHtml) ? css.articleZh : css.articleEn}`, dangerouslySetInnerHTML: { __html: articleHtml } })), (articleTruncated || openEntry.truncated === true) && (_jsxs("p", { className: css.incomplete, children: [t('detail.incomplete'), " \u2014", ' ', _jsx("button", { type: "button", className: css.incompleteLink, onClick: () => { if (openEntry.link !== undefined)
2882
2927
  props.openExternal(openEntry.link); }, children: t('detail.readOriginal') })] })), _jsxs("div", { className: css.tagEdit, children: [_jsx("span", { className: css.tagEditLabel, children: t('tag.title') }), tags.filter(tag => (entryTagIds[openEntry.id] ?? []).includes(tag.id)).map(tag => (_jsxs("span", { className: css.tagChip, children: [tag.name, _jsx("button", { type: "button", className: css.tagChipRemove, title: t('action.remove'), onClick: () => { void toggleTag(openEntry.id, tag.id, false); }, children: "\u00D7" })] }, tag.id))), tagInputOpen
2883
2928
  ? (_jsxs("div", { className: css.tagEditField, children: [_jsx("input", { autoFocus: true, className: css.tagInput, value: tagDraft, placeholder: t('tag.placeholder'), onChange: event => setTagDraft(event.target.value),
2884
2929
  // A suggestion's mousedown is suppressed, so this fires only
@@ -10,7 +10,7 @@
10
10
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
11
11
  import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
12
12
  import type { GlobalStandardProps, InjectFace, PropsLocale, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
13
- import type { ReaderAddFailure, ReaderAddOutcome, ReaderAddRefusal, ReaderAnnotationOutcome, ReaderBackfillCandidate, ReaderEntryBodyView, ReaderEntryFetchState, ReaderBody, ReaderCapabilities, ReaderEntryTranslationView, ReaderMutationOutcome, ReaderRefreshResult, ReaderRecentEntry, ReaderSentenceLearn, ReaderSourceSummary, ReaderStorageStats, ReaderTag } from '../types.ts';
13
+ import type { ReaderAddFailure, ReaderAddOutcome, ReaderAddRefusal, ReaderAnnotationOutcome, ReaderBackfillCandidate, ReaderEntryBodyView, ReaderEntryFetchState, ReaderBody, ReaderCapabilities, ReaderEntryTranslationView, ReaderImageResult, ReaderMutationOutcome, ReaderRefreshResult, ReaderRecentEntry, ReaderSentenceLearn, ReaderSourceSummary, ReaderStorageStats, ReaderTag } from '../types.ts';
14
14
  import type { createReaderStore } from './store.ts';
15
15
  export type { RemoteResult };
16
16
  /** The result of extracting one article, as the detail view consumes it. */
@@ -143,6 +143,14 @@ export interface ReaderPaneInjected {
143
143
  * extracted title/excerpt ride along for the card upgrade.
144
144
  */
145
145
  fetchEntryBody: (entryId: string, url: string) => Promise<ReaderEntryBodyView & ReaderExtractedMeta>;
146
+ /**
147
+ * Fetch one image through the host, for an `<img>` the browser already
148
+ * failed on (CORP/hotlink protection — the bytes answer 200 to a direct
149
+ * fetch but are withheld from a cross-origin page). The pane re-points the
150
+ * image at a `data:` URI built from the answer; a host without the verb (or
151
+ * an origin that refuses the host too) leaves the broken image alone.
152
+ */
153
+ fetchImage: (url: string) => Promise<RemoteResult<ReaderImageResult>>;
146
154
  /** The tags on one entry. */
147
155
  entryTags: (entryId: string) => Promise<RemoteResult<{
148
156
  tags: ReaderTag[];
@@ -145,6 +145,7 @@ export async function apply(ctx) {
145
145
  listBackfillCandidates: entries => remote.listBackfillCandidates({ entries }),
146
146
  getEntryBody: request => remote.getEntryBody(request),
147
147
  fetchEntryBody,
148
+ fetchImage: url => remote.fetchImage({ url }),
148
149
  getEntryTranslation: entryId => remote.getEntryTranslation({ entryId }),
149
150
  getSentenceTranslations: request => remote.getSentenceTranslations(request),
150
151
  rememberSentences: request => remote.rememberSentences(request),
@@ -0,0 +1,81 @@
1
+ import type { LookupFunction } from 'node:net';
2
+ /** The outcome of one image fetch: the bytes and their type, or the reason not. */
3
+ export type ImageFetchOutcome = {
4
+ readonly ok: true;
5
+ readonly url: string;
6
+ readonly mime: string;
7
+ readonly base64: string;
8
+ } | {
9
+ readonly ok: false;
10
+ readonly url: string;
11
+ readonly error: string;
12
+ };
13
+ /** One resolved host address (the `dns.lookup` all-mode shape). */
14
+ export interface ImageResolvedAddress {
15
+ readonly address: string;
16
+ readonly family: number;
17
+ }
18
+ /** Injectable resolver seam — the POLICY half's DNS (defaults to the system resolver). */
19
+ export type ImageHostResolver = (hostname: string) => Promise<readonly ImageResolvedAddress[]>;
20
+ /** The transport half's address supply — node's own `lookup` callback contract. */
21
+ export type ImageTransportLookup = LookupFunction;
22
+ /**
23
+ * The two seams a test injects: `resolve` answers what the POLICY validates
24
+ * (a public address), `lookup` answers what the CONNECTION dials (the fixture
25
+ * server's loopback). In production both default to the system resolver and
26
+ * the lookup is the validated answer set — that identity IS the pin.
27
+ */
28
+ export interface ImageFetchSeams {
29
+ readonly resolve?: ImageHostResolver;
30
+ readonly lookup?: ImageTransportLookup;
31
+ }
32
+ /** Numeric limits, overridable in tests. */
33
+ export interface ImageFetchLimits {
34
+ readonly maxBytes?: number;
35
+ readonly timeoutMs?: number;
36
+ readonly maxRedirects?: number;
37
+ }
38
+ /** An article figure is a few hundred KB; 8 MiB leaves room for print-resolution PNGs. */
39
+ export declare const IMAGE_FETCH_MAX_BYTES: number;
40
+ /** One image fetch never hangs the rescue: the pane's picture stays broken instead. */
41
+ export declare const IMAGE_FETCH_TIMEOUT_MS = 15000;
42
+ /** Redirect hops per image, each re-validated end to end (the classic SSRF hop). */
43
+ export declare const IMAGE_FETCH_MAX_REDIRECTS = 3;
44
+ /**
45
+ * The byte budget of the in-memory rescue cache (wire size — base64 is what
46
+ * the map actually holds). A figure-heavy article is a few MB; the budget
47
+ * covers several of them, and the oldest entry leaves first.
48
+ */
49
+ export declare const IMAGE_CACHE_BUDGET_BYTES: number;
50
+ /**
51
+ * The rescue cache: a pane remount (the sidebar unmounts wholesale when
52
+ * another panel takes over) re-fails every CORP-blocked image, and this map is
53
+ * what keeps that a memory read instead of a dozen outbound requests. LRU by
54
+ * insertion refresh, bounded by bytes.
55
+ */
56
+ export declare class ImageFetchCache {
57
+ private readonly budgetBytes;
58
+ private readonly entries;
59
+ private bytes;
60
+ constructor(budgetBytes?: number);
61
+ /** The cached bytes for one URL, refreshing its recency. */
62
+ get(url: string): {
63
+ mime: string;
64
+ base64: string;
65
+ } | undefined;
66
+ /** Cache one fetched image, evicting the oldest entries until it fits. */
67
+ set(url: string, mime: string, base64: string): void;
68
+ }
69
+ /**
70
+ * Fetch one image, policy-checked end to end.
71
+ *
72
+ * Errors are values: the pane's only answer to a failed rescue is leaving the
73
+ * broken image as it was, so the reason string is for logs, not for display.
74
+ *
75
+ * @param rawUrl - the image URL the pane failed on.
76
+ * @param seams - resolver/lookup overrides (tests only; production pins).
77
+ * @param limits - byte/timeout/redirect overrides (tests only).
78
+ * @returns the image bytes base64-encoded with their declared type, or why not.
79
+ */
80
+ export declare function fetchImageBytes(rawUrl: string, seams?: ImageFetchSeams, limits?: ImageFetchLimits): Promise<ImageFetchOutcome>;
81
+ //# sourceMappingURL=image-fetch.d.ts.map