@a9n-shoji/rvw 0.2.2 → 0.2.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.
Files changed (99) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +6 -3
  3. package/dist/cli.mjs +188 -19
  4. package/dist/cli.mjs.map +4 -4
  5. package/dist/web/assets/DocumentViewer-B4LdSL1l.js +1560 -0
  6. package/dist/web/assets/{MarkdownTable-DVJ0_MIC.js → MarkdownTable-CdbKPft2.js} +1 -1
  7. package/dist/web/assets/WalkthroughViewer-D1LlSQw2.js +1 -0
  8. package/dist/web/assets/{abnfDiagram-N423BO3Z-6dQzEuPV.js → abnfDiagram-N423BO3Z-CE1nZSC9.js} +1 -1
  9. package/dist/web/assets/architecture-TIHT7OUA-edLV2Zq-.js +1 -0
  10. package/dist/web/assets/{architectureDiagram-T3A2C74G-DP7hHV7T.js → architectureDiagram-T3A2C74G-B88qYFUo.js} +1 -1
  11. package/dist/web/assets/{blockDiagram-VBNYF7ZC-ZbwzVKWP.js → blockDiagram-VBNYF7ZC-BaX0rhbb.js} +1 -1
  12. package/dist/web/assets/{c4Diagram-5PPSVZJV-HVD9_uV4.js → c4Diagram-5PPSVZJV-C1ppBaEC.js} +1 -1
  13. package/dist/web/assets/channel-J8FHSJDG.js +1 -0
  14. package/dist/web/assets/{chunk-2GRJ4B5K-Bkxs2d4J.js → chunk-2GRJ4B5K-D2TTocfh.js} +1 -1
  15. package/dist/web/assets/{chunk-4I5QYGJK-BJHE6zC_.js → chunk-4I5QYGJK-ikT25rPN.js} +1 -1
  16. package/dist/web/assets/{chunk-5RXB4S5H-B-52dfCQ.js → chunk-5RXB4S5H-CGlPaulj.js} +1 -1
  17. package/dist/web/assets/{chunk-6Q2QTUOP-BpD0vQ66.js → chunk-6Q2QTUOP-BuuMeCo2.js} +1 -1
  18. package/dist/web/assets/{chunk-7Z6QIM7H-BTI1qgAO.js → chunk-7Z6QIM7H-DT3BAZ4L.js} +1 -1
  19. package/dist/web/assets/{chunk-GF5L2VYU-B8BxoCCx.js → chunk-GF5L2VYU-mIdlxzfr.js} +1 -1
  20. package/dist/web/assets/{chunk-I66GZJ75-zwi-Xf01.js → chunk-I66GZJ75-CGl1hx4J.js} +1 -1
  21. package/dist/web/assets/{chunk-JQJVKLGR-D9mKBWUz.js → chunk-JQJVKLGR-BLG6gAfx.js} +1 -1
  22. package/dist/web/assets/{chunk-KBJHAD2P-Qmtir3qc.js → chunk-KBJHAD2P-D_MPzBJx.js} +1 -1
  23. package/dist/web/assets/{chunk-NSK5VX7P-SP2vZdAo.js → chunk-NSK5VX7P-CiYiU8wP.js} +1 -1
  24. package/dist/web/assets/{chunk-QR6OTTB3-uVDCHurs.js → chunk-QR6OTTB3-BK8Tnvcc.js} +1 -1
  25. package/dist/web/assets/{chunk-UBXNYLIW-CtNzBWOU.js → chunk-UBXNYLIW-BuzttTFk.js} +1 -1
  26. package/dist/web/assets/{chunk-W5SLKNZC-BPfDqbmK.js → chunk-W5SLKNZC-UidmFHe2.js} +1 -1
  27. package/dist/web/assets/{chunk-WRU74C26-BZkEOexq.js → chunk-WRU74C26-nox7WFnh.js} +1 -1
  28. package/dist/web/assets/classDiagram-JCYQIIEL-CmTWSnH0.js +1 -0
  29. package/dist/web/assets/classDiagram-v2-OCEON4UE-CmTWSnH0.js +1 -0
  30. package/dist/web/assets/{cynefin-VYW2F7L2-BRisCyqC.js → cynefin-VYW2F7L2-wDMibS5s.js} +1 -1
  31. package/dist/web/assets/{cynefinDiagram-MW4NZA55-ClIrmCkB.js → cynefinDiagram-MW4NZA55-CBTrdvOE.js} +1 -1
  32. package/dist/web/assets/{dagre-VZM6K2ZE-X31uC_JB.js → dagre-VZM6K2ZE-Bbgn4Rr7.js} +1 -1
  33. package/dist/web/assets/{diagram-7IWD3JNH-DBQFjl0i.js → diagram-7IWD3JNH-Bm9AuXb9.js} +1 -1
  34. package/dist/web/assets/{diagram-B4RE2ZJO-BZ7934Wg.js → diagram-B4RE2ZJO-D3lZ-r-H.js} +1 -1
  35. package/dist/web/assets/{diagram-LBJQPF4R-CcJqH9aY.js → diagram-LBJQPF4R-D-GaynX7.js} +1 -1
  36. package/dist/web/assets/{diagram-Q27KOJAE-DzM7eiil.js → diagram-Q27KOJAE-BeZRiu1d.js} +1 -1
  37. package/dist/web/assets/{diagram-UB23O5K3-CVCV-d3m.js → diagram-UB23O5K3-C3GP7iMe.js} +1 -1
  38. package/dist/web/assets/{ebnfDiagram-BXEA7PRR-D8xb9FBp.js → ebnfDiagram-BXEA7PRR-BJWg0NVU.js} +1 -1
  39. package/dist/web/assets/{erDiagram-JOGREHBK-1l-G_PMG.js → erDiagram-JOGREHBK-XPmhUeB_.js} +1 -1
  40. package/dist/web/assets/eventmodeling-45OFAUF4-C3c4qddw.js +1 -0
  41. package/dist/web/assets/flowDiagram-UKHOOZJN-DMA-a1zf.js +1 -0
  42. package/dist/web/assets/{ganttDiagram-PKOTCBZU-CDJ9a1tp.js → ganttDiagram-PKOTCBZU-CL-xEo-M.js} +1 -1
  43. package/dist/web/assets/{gitGraph-TEB2WS4Q-DOeTh5wB.js → gitGraph-TEB2WS4Q-BVRyuIx3.js} +1 -1
  44. package/dist/web/assets/{gitGraphDiagram-DS77QQ5N-DYLxc8Nz.js → gitGraphDiagram-DS77QQ5N-BVNIEupW.js} +1 -1
  45. package/dist/web/assets/{index-9cOaPesh.js → index-D8GJBw23.js} +98 -98
  46. package/dist/web/assets/{index-kK3hngnz.css → index-DMD2vvQW.css} +1 -1
  47. package/dist/web/assets/{info-DKCQHKI2-B5u97QuQ.js → info-DKCQHKI2-BKBUVpfc.js} +1 -1
  48. package/dist/web/assets/{infoDiagram-6WML65LV-zosKW2OD.js → infoDiagram-6WML65LV-_dTFnzSE.js} +1 -1
  49. package/dist/web/assets/{ishikawaDiagram-WSZJBQD7-VYcc_au8.js → ishikawaDiagram-WSZJBQD7-DIRgD4hO.js} +1 -1
  50. package/dist/web/assets/{journeyDiagram-NVQOT4AX-C_nU8kP4.js → journeyDiagram-NVQOT4AX-bp7nZrfR.js} +1 -1
  51. package/dist/web/assets/{kanban-definition-27J2QSJJ-DybKaWfx.js → kanban-definition-27J2QSJJ-C6mZIo8S.js} +1 -1
  52. package/dist/web/assets/{line-DWsGplGj.js → line-DWTym7eJ.js} +1 -1
  53. package/dist/web/assets/{mermaid-parser.core-D_T_3oIQ.js → mermaid-parser.core-CbecsNA1.js} +3 -3
  54. package/dist/web/assets/{mermaid.core-BoLGzxB5.js → mermaid.core-CdRuLyYI.js} +4 -4
  55. package/dist/web/assets/{mindmap-definition-FAOFIHXS-CsdpqPIa.js → mindmap-definition-FAOFIHXS-CU2P1z-9.js} +1 -1
  56. package/dist/web/assets/{packet-7NZHBO7P-CReAEF1h.js → packet-7NZHBO7P-CZsup6Od.js} +1 -1
  57. package/dist/web/assets/{pegDiagram-VL7TDLO6-CE9-NPMu.js → pegDiagram-VL7TDLO6-DTTyttpe.js} +1 -1
  58. package/dist/web/assets/{pie-RZYD4A2V-GYixXUka.js → pie-RZYD4A2V-DgrIiWZA.js} +1 -1
  59. package/dist/web/assets/{pieDiagram-7S7Q4E2Y-CqaAKNg2.js → pieDiagram-7S7Q4E2Y-BBNFyPT1.js} +1 -1
  60. package/dist/web/assets/{quadrantDiagram-CIZ2JOQS-eRMHJqfj.js → quadrantDiagram-CIZ2JOQS-ejp-Aa2l.js} +1 -1
  61. package/dist/web/assets/{radar-I7S5WNFK-BwHxP0iK.js → radar-I7S5WNFK-Cp_hfjrI.js} +1 -1
  62. package/dist/web/assets/{railroad-3IZDKUUU-AHc3O4Oz.js → railroad-3IZDKUUU-Dw-oMal2.js} +1 -1
  63. package/dist/web/assets/railroad-abnf-AHOZXSZD-BN-ZBhE0.js +1 -0
  64. package/dist/web/assets/railroad-ebnf-EBAXGLYW-CG97zyI2.js +1 -0
  65. package/dist/web/assets/railroad-peg-LSFZ7HO6-DHLQ1evQ.js +1 -0
  66. package/dist/web/assets/{railroadDiagram-AXF67PYL-BhAxCokG.js → railroadDiagram-AXF67PYL-DkBUaj6Z.js} +1 -1
  67. package/dist/web/assets/{requirementDiagram-LRYGKXZP-BRW-kYhg.js → requirementDiagram-LRYGKXZP-Dff1ex_h.js} +1 -1
  68. package/dist/web/assets/{sankeyDiagram-W5VNT64P-DfnYLbDJ.js → sankeyDiagram-W5VNT64P-DptSL9Bc.js} +1 -1
  69. package/dist/web/assets/{sequenceDiagram-SI44F4Z6-WEkIPSg8.js → sequenceDiagram-SI44F4Z6-DEnPcs3U.js} +1 -1
  70. package/dist/web/assets/{stateDiagram-OKZ733FA-CdN7veIN.js → stateDiagram-OKZ733FA-BwJO1MKA.js} +1 -1
  71. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-LaSNqvWy.js +1 -0
  72. package/dist/web/assets/{swimlanes-SLNWSIFB-g9wWLf5Y.js → swimlanes-SLNWSIFB-BosKiwZA.js} +1 -1
  73. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-C5OBtBlv.js +8 -0
  74. package/dist/web/assets/{timeline-definition-Z64GVDOM-DjkKZIPE.js → timeline-definition-Z64GVDOM-C5isAn4X.js} +1 -1
  75. package/dist/web/assets/{treeView-QDETBFTQ-WARa9r09.js → treeView-QDETBFTQ-zEyUXRc4.js} +1 -1
  76. package/dist/web/assets/{treemap-6X3UGDF4-CTFjdb6k.js → treemap-6X3UGDF4-B-X0EQSE.js} +1 -1
  77. package/dist/web/assets/{vennDiagram-T6HMQDX7-CI9QrBwu.js → vennDiagram-T6HMQDX7-QMdGieCa.js} +1 -1
  78. package/dist/web/assets/{wardley-OPB4EBWU-Cfo_Nnla.js → wardley-OPB4EBWU-BaeAF8d8.js} +1 -1
  79. package/dist/web/assets/{wardleyDiagram-T6FBY63Y-CzGJ4fzZ.js → wardleyDiagram-T6FBY63Y-DTQ_uLUq.js} +1 -1
  80. package/dist/web/assets/{xychartDiagram-ELKLHX3M-BXCiD7gJ.js → xychartDiagram-ELKLHX3M-BMdBWzib.js} +1 -1
  81. package/dist/web/index.html +2 -2
  82. package/package.json +1 -1
  83. package/skills/rvw-watch-comments/SKILL.md +84 -34
  84. package/skills/rvw-watch-comments/agents/openai.yaml +2 -2
  85. package/skills/rvw-watch-comments/scripts/watch-driver.mjs +223 -48
  86. package/skills/rvw-watch-comments/scripts/watch-state.mjs +31 -9
  87. package/dist/web/assets/DocumentViewer-ADzspLC4.js +0 -1560
  88. package/dist/web/assets/WalkthroughViewer-yQibj2tK.js +0 -1
  89. package/dist/web/assets/architecture-TIHT7OUA-DEoBuEnc.js +0 -1
  90. package/dist/web/assets/channel-DjgdV-zW.js +0 -1
  91. package/dist/web/assets/classDiagram-JCYQIIEL-B6Qjam7Y.js +0 -1
  92. package/dist/web/assets/classDiagram-v2-OCEON4UE-B6Qjam7Y.js +0 -1
  93. package/dist/web/assets/eventmodeling-45OFAUF4-ndNeUBOZ.js +0 -1
  94. package/dist/web/assets/flowDiagram-UKHOOZJN-BVxK_6Tu.js +0 -1
  95. package/dist/web/assets/railroad-abnf-AHOZXSZD-ZKY5olmV.js +0 -1
  96. package/dist/web/assets/railroad-ebnf-EBAXGLYW-CrG1UCMo.js +0 -1
  97. package/dist/web/assets/railroad-peg-LSFZ7HO6-CoY6MIbj.js +0 -1
  98. package/dist/web/assets/stateDiagram-v2-UEYNNEHI-0xn0gbpn.js +0 -1
  99. package/dist/web/assets/swimlanesDiagram-ULZ7WXOC-YJ3yh_d3.js +0 -8
package/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.2.4] - 2026-08-24
9
+
10
+ ### Changed
11
+
12
+ - `rvw-watch-comments`のinvestigate-and-reply専用taskで、同じPR/repositoryの後続batchもworker capacity内で
13
+ 並列に調査・返信。保証できる場合は`max-in-flight=8`を目標とし、fix-and-pushを許可したtaskだけ従来の
14
+ writer排他を維持
15
+
16
+ ## [0.2.3] - 2026-08-21
17
+
18
+ ### Added
19
+
20
+ - PR本文へ貼り付けたmodern GitHub user attachment画像を、public/private共通のlocalhost proxyで表示
21
+ - repositoryのPNG、JPEG、GIF、WebP、AVIF、SVGをexact commit固定の全文表示とold/new Split表示で閲覧
22
+ - 画像ファイルのadded、deleted、renamed、画像・非画像間の変更表示とfile-level comment
23
+
24
+ ### Fixed
25
+
26
+ - 同じtask stateで`rvw-watch-comments`のwatch driverを複数起動できないようにし、二重監視と重複処理を防止
27
+
28
+ ### Security
29
+
30
+ - GitHub attachment proxyを厳密な`https://github.com/user-attachments/assets/<uuid>`だけへ限定し、
31
+ 同一origin request、GitHub CLIの既存認証、10 MiB上限、magic-byte判定、`nosniff`、SVG sandbox CSPを適用
32
+ - arbitrary external image、legacy GitHub image host、unknown binary、HTML/JSON error responseは引き続き自動取得しない
33
+
8
34
  ## [0.2.2] - 2026-08-21
9
35
 
10
36
  ### Changed
package/README.md CHANGED
@@ -118,8 +118,8 @@ rvw walkthrough publish --stdin --json
118
118
  構成されます。参照は意味のある複数行range、単行、またはfile全体を指せます。rvwは登録時にcommit、
119
119
  path、指定された行範囲を検証します。publishしてもbrowserは開かれず、
120
120
  active tabやscroll位置も変わりません。人間がviewerのWalkthroughを開き、必要なreferenceを選んだ
121
- 時だけ、exact sourceがdocument tabへ開き、行指定があれば範囲全体が強調されます。`Cmd` / `Ctrl`を押しながら選べば
122
- 操作元と反対のペインへ開きます。説明tabは残るため、
121
+ 時だけ、exact sourceがdocument tabへ開き、行指定があれば範囲全体が強調されます。通常clickは左ペイン、
122
+ `Cmd` / `Ctrl`を押しながら選べば右ペインへ開きます。説明tabは残るため、
123
123
  複数のclaimと実装を任意の順序・任意のタイミングで往復できます。
124
124
 
125
125
  Walkthrough本文のinline referenceとMermaid node linkは維持しますが、同じ参照を横や下へ列挙する
@@ -199,7 +199,10 @@ PRは常にcode/GitHub read-onlyで調査します。同梱driverがcursor resum
199
199
  batchをclaimすると各threadへ`🔎 確認中です…`をAgent往復なしに即時返信し、
200
200
  完了時は同じreplyを最終結果へ編集します。Skill同梱のtask-state toolがrepository外のSQLiteへcursor、
201
201
  queue、retry、batch内のthread単位status post、自己返信抑制をtransactionalに保存します。同じthreadへ
202
- 後から返信が追加された場合は新しいstatus postを返信するため、以前の回答は書き換えません。
202
+ 後から返信が追加された場合は新しいstatus postを返信するため、以前の回答は書き換えません。親taskは
203
+ 起動前にsubagent枠を予約し、driverはその枠数までだけbatchをclaimします。acknowledge済みbatchは大小や
204
+ 変更有無にかかわらず同じscheduling turnでfresh subagentへ委譲し、親taskは直接処理しません。lease解放後の
205
+ 同一PR follow-upと期限到達したretryはdriverがtask stateから自動的に再開します。
203
206
  調査結果、実装内容、test結果が具体的なcodeに基づく場合、Skillは最終replyからexact commitの有用な
204
207
  line rangeへ`rvw-ref:` linkを付け、reviewerが根拠へ直接移動できるようにします。
205
208
 
package/dist/cli.mjs CHANGED
@@ -19759,12 +19759,15 @@ import path5 from "node:path";
19759
19759
  import { TextDecoder as TextDecoder2 } from "node:util";
19760
19760
 
19761
19761
  // src/shared/constants.ts
19762
- var APP_VERSION = "0.2.2";
19762
+ var APP_VERSION = "0.2.4";
19763
19763
  var PROTOCOL_VERSION = 3;
19764
19764
  var VIEWER_ID_HEADER = "x-rvw-viewer-id";
19765
19765
  var GIT_OBJECT_ID_PATTERN = /^[0-9a-f]{40,64}$/i;
19766
19766
  var MAX_TEXT_DOCUMENT_BYTES = 1024 * 1024;
19767
19767
  var MAX_MARKDOWN_ASSET_BYTES = 5 * 1024 * 1024;
19768
+ var MAX_GITHUB_ATTACHMENT_BYTES = 10 * 1024 * 1024;
19769
+ var MAX_GITHUB_ATTACHMENT_STDERR_BYTES = 64 * 1024;
19770
+ var GITHUB_ATTACHMENT_TIMEOUT_MS = 3e4;
19768
19771
  var MAX_COMMENT_BODY_BYTES = 64 * 1024;
19769
19772
  var MAX_AUTHOR_LABEL_CHARACTERS = 100;
19770
19773
  var MAX_IDEMPOTENCY_KEY_CHARACTERS = 200;
@@ -20414,6 +20417,101 @@ var GitClient = class {
20414
20417
  }
20415
20418
  };
20416
20419
 
20420
+ // src/shared/image-assets.ts
20421
+ var SUPPORTED_IMAGE_EXTENSIONS = [
20422
+ ".png",
20423
+ ".jpg",
20424
+ ".jpeg",
20425
+ ".gif",
20426
+ ".webp",
20427
+ ".avif",
20428
+ ".svg"
20429
+ ];
20430
+ var githubAttachmentPathPattern = /^\/user-attachments\/assets\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
20431
+ var utf8Fatal2 = new TextDecoder("utf-8", { fatal: true });
20432
+ function hasBytes(bytes, offset, expected) {
20433
+ return expected.every((value, index2) => bytes[offset + index2] === value);
20434
+ }
20435
+ function ascii(bytes, offset, length) {
20436
+ return String.fromCharCode(...bytes.subarray(offset, offset + length));
20437
+ }
20438
+ function uint16Le(bytes, offset) {
20439
+ return (bytes[offset] ?? 0) | (bytes[offset + 1] ?? 0) << 8;
20440
+ }
20441
+ function uint32Be(bytes, offset) {
20442
+ return (bytes[offset] ?? 0) * 16777216 + ((bytes[offset + 1] ?? 0) << 16) + ((bytes[offset + 2] ?? 0) << 8) + (bytes[offset + 3] ?? 0) >>> 0;
20443
+ }
20444
+ function uint32Le(bytes, offset) {
20445
+ return (bytes[offset] ?? 0) + ((bytes[offset + 1] ?? 0) << 8) + ((bytes[offset + 2] ?? 0) << 16) + (bytes[offset + 3] ?? 0) * 16777216 >>> 0;
20446
+ }
20447
+ function isSvg(bytes) {
20448
+ if (bytes.length === 0) return false;
20449
+ let text3;
20450
+ try {
20451
+ text3 = utf8Fatal2.decode(bytes);
20452
+ } catch {
20453
+ return false;
20454
+ }
20455
+ const withoutBom = text3.charCodeAt(0) === 65279 ? text3.slice(1) : text3;
20456
+ let candidate = withoutBom.trimStart();
20457
+ const declaration = /^<\?xml(?:\s[^?]*)?\?>/iu.exec(candidate);
20458
+ if (declaration) candidate = candidate.slice(declaration[0].length).trimStart();
20459
+ while (candidate.startsWith("<!--")) {
20460
+ const commentEnd = candidate.indexOf("-->");
20461
+ if (commentEnd < 0) return false;
20462
+ candidate = candidate.slice(commentEnd + 3).trimStart();
20463
+ }
20464
+ return /^<svg(?:\s|>)/iu.test(candidate);
20465
+ }
20466
+ function isSupportedImagePath(filePath) {
20467
+ const normalized = filePath.toLowerCase();
20468
+ return SUPPORTED_IMAGE_EXTENSIONS.some((extension2) => normalized.endsWith(extension2));
20469
+ }
20470
+ function canonicalGitHubAttachmentUrl(value) {
20471
+ if (!value?.startsWith("https://github.com/")) return null;
20472
+ let parsed;
20473
+ try {
20474
+ parsed = new URL(value);
20475
+ } catch {
20476
+ return null;
20477
+ }
20478
+ if (parsed.protocol !== "https:" || parsed.hostname !== "github.com" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "" || parsed.search !== "" || parsed.hash !== "" || !githubAttachmentPathPattern.test(parsed.pathname)) {
20479
+ return null;
20480
+ }
20481
+ return parsed.href;
20482
+ }
20483
+ function detectImageContentType(bytes) {
20484
+ if (bytes.length >= 33 && hasBytes(bytes, 0, [137, 80, 78, 71, 13, 10, 26, 10]) && ascii(bytes, 12, 4) === "IHDR" && uint32Be(bytes, 16) > 0 && uint32Be(bytes, 20) > 0) {
20485
+ return "image/png";
20486
+ }
20487
+ if (bytes.length >= 10 && hasBytes(bytes, 0, [255, 216, 255]) && hasBytes(bytes, bytes.length - 2, [255, 217])) {
20488
+ return "image/jpeg";
20489
+ }
20490
+ const gifHeader = bytes.length >= 13 ? ascii(bytes, 0, 6) : "";
20491
+ if ((gifHeader === "GIF87a" || gifHeader === "GIF89a") && uint16Le(bytes, 6) > 0 && uint16Le(bytes, 8) > 0) {
20492
+ return "image/gif";
20493
+ }
20494
+ if (bytes.length >= 20 && ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WEBP" && ["VP8 ", "VP8L", "VP8X"].includes(ascii(bytes, 12, 4)) && uint32Le(bytes, 4) >= 12 && uint32Le(bytes, 4) + 8 <= bytes.length) {
20495
+ return "image/webp";
20496
+ }
20497
+ if (bytes.length >= 16 && ascii(bytes, 4, 4) === "ftyp") {
20498
+ const boxSize = uint32Be(bytes, 0);
20499
+ if (boxSize >= 16 && boxSize <= bytes.length) {
20500
+ const brands = [ascii(bytes, 8, 4)];
20501
+ for (let offset = 16; offset + 4 <= boxSize; offset += 4) {
20502
+ brands.push(ascii(bytes, offset, 4));
20503
+ }
20504
+ if (brands.some((brand) => brand === "avif" || brand === "avis")) {
20505
+ return "image/avif";
20506
+ }
20507
+ }
20508
+ }
20509
+ return isSvg(bytes) ? "image/svg+xml" : null;
20510
+ }
20511
+ function imageContentTypeHeader(contentType2) {
20512
+ return contentType2 === "image/svg+xml" ? `${contentType2}; charset=utf-8` : contentType2;
20513
+ }
20514
+
20417
20515
  // src/infrastructure/github/github-client.ts
20418
20516
  var ghPullRequestSchema = external_exports.object({
20419
20517
  author: external_exports.object({ login: external_exports.string().min(1) }).nullable(),
@@ -20439,6 +20537,10 @@ function parsePullRequestUrl(url2) {
20439
20537
  return { owner: match2[1], repository: match2[2], number: Number(match2[3]) };
20440
20538
  }
20441
20539
  var GitHubClient = class {
20540
+ constructor(processRunner = runProcess) {
20541
+ this.processRunner = processRunner;
20542
+ }
20543
+ processRunner;
20442
20544
  async doctor() {
20443
20545
  const version2 = await runText("gh", ["--version"]);
20444
20546
  const auth = await runProcess("gh", ["auth", "status", "--hostname", "github.com"], {
@@ -20523,6 +20625,31 @@ var GitHubClient = class {
20523
20625
  isDraft: parsed.data.isDraft
20524
20626
  };
20525
20627
  }
20628
+ async getAttachment(absoluteUrl) {
20629
+ const canonicalUrl = canonicalGitHubAttachmentUrl(absoluteUrl);
20630
+ if (!canonicalUrl) {
20631
+ throw new RvwError("INVALID_INPUT", "GitHub user attachment URL\u304C\u4E0D\u6B63\u3067\u3059\u3002");
20632
+ }
20633
+ try {
20634
+ const result = await this.processRunner("gh", ["api", canonicalUrl], {
20635
+ timeoutMs: GITHUB_ATTACHMENT_TIMEOUT_MS,
20636
+ maxStdoutBytes: MAX_GITHUB_ATTACHMENT_BYTES,
20637
+ maxStderrBytes: MAX_GITHUB_ATTACHMENT_STDERR_BYTES
20638
+ });
20639
+ return { content: result.stdout, byteLength: result.stdout.byteLength };
20640
+ } catch (error51) {
20641
+ if (error51 instanceof RvwError && error51.code === "PROCESS_OUTPUT_LIMIT" && error51.details?.stream === "stdout") {
20642
+ throw new RvwError("FILE_TOO_LARGE", "GitHub attachment\u306F10 MiB\u4EE5\u4E0B\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002", {
20643
+ status: 413
20644
+ });
20645
+ }
20646
+ throw new RvwError(
20647
+ "GITHUB_ERROR",
20648
+ "GitHub attachment\u3092\u53D6\u5F97\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002gh\u8A8D\u8A3C\u3068\u753B\u50CF\u306E\u95B2\u89A7\u6A29\u9650\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
20649
+ { status: 502 }
20650
+ );
20651
+ }
20652
+ }
20526
20653
  };
20527
20654
 
20528
20655
  // src/application/rvw-service.ts
@@ -28398,6 +28525,21 @@ var RvwService = class {
28398
28525
  await this.assertCommitAvailable(pullRequest, sourceOid);
28399
28526
  return await this.git.readRepositoryAsset(pullRequest.localRepositoryPath, sourceOid, filePath);
28400
28527
  }
28528
+ async getGitHubAttachment(pullRequestId, absoluteUrl) {
28529
+ this.getPullRequest(pullRequestId);
28530
+ const canonicalUrl = canonicalGitHubAttachmentUrl(absoluteUrl);
28531
+ if (!canonicalUrl) {
28532
+ throw new RvwError("INVALID_INPUT", "GitHub user attachment URL\u304C\u4E0D\u6B63\u3067\u3059\u3002");
28533
+ }
28534
+ const attachment = await this.github.getAttachment(canonicalUrl);
28535
+ const contentType2 = detectImageContentType(attachment.content);
28536
+ if (!contentType2) {
28537
+ throw new RvwError("UNSUPPORTED_IMAGE", "GitHub attachment\u306F\u5BFE\u5FDC\u753B\u50CF\u5F62\u5F0F\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002", {
28538
+ status: 415
28539
+ });
28540
+ }
28541
+ return { ...attachment, contentType: contentType2 };
28542
+ }
28401
28543
  async getDiff(ref) {
28402
28544
  const [old, next] = await Promise.all([
28403
28545
  ref.old ? this.getDocument(ref.old) : Promise.resolve(null),
@@ -33257,17 +33399,25 @@ function parseBooleanQuery(value, name) {
33257
33399
  if (value === "true") return true;
33258
33400
  throw new RvwError("INVALID_INPUT", `${name} query\u306Ftrue\u307E\u305F\u306Ffalse\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002`);
33259
33401
  }
33260
- function markdownAssetContentType(filePath) {
33261
- const extension2 = path8.extname(filePath).toLowerCase();
33262
- return {
33263
- ".avif": "image/avif",
33264
- ".gif": "image/gif",
33265
- ".jpeg": "image/jpeg",
33266
- ".jpg": "image/jpeg",
33267
- ".png": "image/png",
33268
- ".svg": "image/svg+xml; charset=utf-8",
33269
- ".webp": "image/webp"
33270
- }[extension2] ?? "application/octet-stream";
33402
+ function repositoryAssetContentType(filePath, content3) {
33403
+ if (!isSupportedImagePath(filePath)) return null;
33404
+ const contentType2 = detectImageContentType(content3);
33405
+ if (!contentType2) {
33406
+ throw new RvwError("UNSUPPORTED_IMAGE", "repository asset\u306F\u5BFE\u5FDC\u753B\u50CF\u5F62\u5F0F\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002", {
33407
+ status: 415
33408
+ });
33409
+ }
33410
+ return contentType2;
33411
+ }
33412
+ function setImageResponseHeaders(setHeader, contentType2) {
33413
+ setHeader("content-type", imageContentTypeHeader(contentType2));
33414
+ setHeader("cache-control", "private, max-age=31536000, immutable");
33415
+ setHeader("x-content-type-options", "nosniff");
33416
+ setHeader("content-disposition", "inline");
33417
+ setHeader("cross-origin-resource-policy", "same-origin");
33418
+ if (contentType2 === "image/svg+xml") {
33419
+ setHeader("content-security-policy", svgAssetContentSecurityPolicy);
33420
+ }
33271
33421
  }
33272
33422
  function documentRefFromQuery(pullRequestId, query) {
33273
33423
  if (query.kind === "pull-request-markdown") {
@@ -33395,17 +33545,36 @@ function createApp(service, options) {
33395
33545
  const ref = documentRefFromQuery(context.req.param("id"), context.req.query());
33396
33546
  return context.json({ ok: true, document: await service.getDocument(ref) });
33397
33547
  });
33398
- app.get("/api/pull-requests/:id/markdown-asset", async (context) => {
33548
+ app.on(["GET", "HEAD"], "/api/pull-requests/:id/markdown-asset", async (context) => {
33399
33549
  const sourceOid = oidQuery(context.req.query("sourceOid"), "sourceOid");
33400
33550
  const filePath = requiredQuery(context.req.query("path"), "path");
33401
33551
  const asset = await service.getRepositoryAsset(context.req.param("id"), sourceOid, filePath);
33402
- context.header("content-type", markdownAssetContentType(filePath));
33403
- context.header("cache-control", "private, max-age=31536000, immutable");
33404
- context.header("x-content-type-options", "nosniff");
33405
- if (path8.extname(filePath).toLowerCase() === ".svg") {
33406
- context.header("content-security-policy", svgAssetContentSecurityPolicy);
33552
+ const contentType2 = repositoryAssetContentType(filePath, asset.content);
33553
+ if (contentType2) {
33554
+ setImageResponseHeaders((name, value) => context.header(name, value), contentType2);
33555
+ } else {
33556
+ context.header("content-type", "application/octet-stream");
33557
+ context.header("cache-control", "private, max-age=31536000, immutable");
33558
+ context.header("x-content-type-options", "nosniff");
33559
+ }
33560
+ return context.req.method === "HEAD" ? context.body(null) : context.body(Uint8Array.from(asset.content));
33561
+ });
33562
+ app.get("/api/pull-requests/:id/github-attachment", async (context) => {
33563
+ const fetchSite = context.req.header("sec-fetch-site");
33564
+ const origin = context.req.header("origin");
33565
+ if (fetchSite !== void 0 && fetchSite !== "same-origin" && fetchSite !== "none" || origin !== void 0 && origin !== options.security.expectedOrigin) {
33566
+ throw new RvwError(
33567
+ "INVALID_ORIGIN",
33568
+ "cross-origin attachment request\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002",
33569
+ {
33570
+ status: 403
33571
+ }
33572
+ );
33407
33573
  }
33408
- return context.body(Uint8Array.from(asset.content));
33574
+ const absoluteUrl = requiredQuery(context.req.query("url"), "url");
33575
+ const attachment = await service.getGitHubAttachment(context.req.param("id"), absoluteUrl);
33576
+ setImageResponseHeaders((name, value) => context.header(name, value), attachment.contentType);
33577
+ return context.body(Uint8Array.from(attachment.content));
33409
33578
  });
33410
33579
  app.get("/api/pull-requests/:id/diff", async (context) => {
33411
33580
  const pullRequestId = context.req.param("id");