@aiquants/daily-report 0.26.0 → 0.28.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,72 @@
2
2
 
3
3
  All notable changes to `@aiquants/daily-report` are documented here.
4
4
 
5
+ ## 0.28.0 (2026-10-05)
6
+
7
+ 0.27.0 の続き: 要求の隔離の許可の一覧化 (別オリジンから受けるのは添付の原本を開く最上位の文書のナビゲーションだけ)、Fetch Metadata の無い要求の `Origin` による判定、拒否の上限付きの記録、添付のどの応答にも枠の方針、配置の格子の単位と E2E のテスト用ハンドルの型の公開、ID 一覧のストリームの行の線形な分割、折り返す文言の改行の規則。どれも README の該当の節が今の契約を記す。
8
+
9
+ ### Security
10
+
11
+ - **別オリジンから受けるのは、添付の原本を開く最上位の文書のナビゲーションだけ**: 要求の隔離は許可の一覧になった。経路ごとの方針 (`RequestIsolationPolicy`) が受けると宣言したナビゲーション (`GET`・`Sec-Fetch-Mode: navigate`・`Sec-Fetch-Dest` がちょうど `document`) だけを通し、それを宣言するのは添付の経路の原本 (インラインとダウンロード。ローダーと同じ厳格なクエリの解釈で決める) だけ。API・SSE・サムネイルは別オリジンからのナビゲーションを受けない。
12
+ 枠 (`iframe`・`frame`)・`fencedframe`・行き先の無いナビゲーション・`HEAD` のナビゲーション (ブラウザーは `HEAD` でナビゲーションしない)・これから仕様に増える行き先は、並べなくても作りから拒む。隔離はハンドラー工場が返す前に 1 つの包みで 4 つの経路へ当て (経路の中の手で置いた判定は無くなった)、添付の経路では形の崩れたクエリも 405 になるメソッドも、別オリジンからなら認証の前に 403 になる (以前は認証の後の 400 か 405)。
13
+ 以前は `object` と `embed` 以外の行き先の GET か HEAD のナビゲーションをどの経路でも通したので、同じサイトの兄弟のサブドメインのページの `<iframe>` が、閲覧者のセッションで、サムネイルのバケットを空にし (閲覧者自身のタイルが 429 になる)、見える添付と見えない添付の応答の時間の差を測り、ID 一覧の全件のクエリを走らせ、SSE の購読を開けた。応答の側の `X-Frame-Options` は枠に描くことを止めるだけで、要求とその仕事は止めない。
14
+ - **Fetch Metadata の無い安全でないメソッドは `Origin` で決める**: ブラウザーは潜在的に信頼できないオリジン (`localhost` 以外の素の HTTP) への要求に `Sec-Fetch-*` を付けないので、ヘッダーの無い要求をすべて通すと、素の HTTP のホストでは兄弟のページのフォームの POST が状態を変えた。`Sec-Fetch-Site` の無い安全でないメソッドは、`Origin` が無ければ通し (ブラウザーのページではない)、要求の宛先のホスト (`new URL(request.url).host`) と同じなら通し、`null`・URL として読めない値・別のホスト・別のポートなら 403 にする (Go の `net/http` の `CrossOriginProtection` と同じ規則)。安全なメソッドは今までどおり通す。隔離を完全にするには、潜在的に信頼できるオリジン (HTTPS) で配る。
15
+ - **添付のどの応答も `X-Frame-Options: SAMEORIGIN`**: 枠への埋め込みの方針は、`nosniff` と `Cross-Origin-Resource-Policy: same-origin` と同じ 1 つの組 (`ATTACHMENT_RESPONSE_SECURITY_HEADERS`) から、失敗の応答 (隔離の 403 を含むどのステータスも。GET と HEAD)・304・2 つの配信の 200 へ付く。以前は 200 だけが持ったので、別オリジンのページは添付の URL を枠に読み込み、塞がれた枠 (見える添付の 200) と描かれた枠 (見えない添付の 404) を見分けられた。
16
+
17
+ ### Added
18
+
19
+ - client の値 `DAILY_REPORT_LAYOUT_LATTICE_PX`: 配置の格子の単位 (4 CSS px)。パッケージ自身の格子の単位そのもので、ホストがビューの周りのバー (ヘッダー・フッターとそれらが分け合うウィンドウ) の大きさをこの倍数にそろえる契約 (README の View height の G-symmetric frame)。公開する配置の値はこれだけで、行の大きさは公開しない。
20
+ - client の型 `DailyReportViewTestHandle`・`DailyReportViewTestReadHandle`・`DailyReportRevealOptions`: E2E のテスト用ハンドル (`__virtualScroll`) の契約とその読み取りの部分、`revealIndex` の揃えの型 (VirtualScroll の `scrollToIndex` の第 2 引数。virtualscroll 3.11.0 の見せる操作 `{ align: "nearest" }` も含む)。型だけの公開なので、バンドルは増えない。ホストのテストは形を書き写さずに読み込む。
21
+
22
+ ### Changed
23
+
24
+ - **拒否は上限付きの記録に残る**: 隔離の拒否は、ハンドラー工場ごとの記録 (`createCrossSiteRejectionRecorder`) が窓 1 つ (`CROSS_SITE_REJECTION_LOG_WINDOW_MS`、60 秒) につき warn の行を 2 行まで書く: 窓の最初の拒否の `cross_site_rejected route=<api|action|sse|attachment> site=<値> mode=<値> dest=<値> method=<メソッド>` と、窓の後の最初の拒否が書く前の窓の数の `cross_site_rejected_suppressed count=<数>` (数えた拒否があるときだけ)。仕様の形 (小文字と `-` だけの 32 文字まで) でない値は、先頭の 32 文字の JSON の文字列で書く。書き先は設定の `logger`、無ければ `[DailyReportIsolation]` を頭に付けた warn の水準の console。受けた要求は窓を開きも閉じもしない。以前は拒否を一切記録しなかったので、同じサイトのページからの探りや CSRF の試みが運用から見えなかった。
25
+ - **ID 一覧のストリームの行の分割は線形**: クライアントは届いたチャンクを復号するたびに、新しく復号した文字だけを改行まで調べ、行が完結したときに欠片を 1 度だけつなぐ (`createNdjsonLineSplitter`)。以前は持ち越した欠け行と新しいチャンクを連結して全体を分け直したので、4,000 件で約 300 KB の行が届き終わるまで、チャンクのたびに同じ前半を調べ直した (同じ形の合成の 16 MB のストリームを 4 KiB のチャンクで分けるベンチで、調べる文字は 1,600 万でなく 6 億、時間は 11.8 ms でなく 287.6 ms)。
26
+ ストリームの終わりの改行の無い行も 1 行として読む (以前は捨てた)。終わりで切れた行は JSON として読めないので、警告 1 件で読み飛ばし、今までどおりカーソルから読み直す。
27
+ - **折り返す文言の改行**: 側面ペインの選択を促す表示と、添付のタイルの表示できない旨の文言は、1 つの規則 (`WRAPPING_LABEL_CLASS_NAME`: `text-wrap: balance` と `word-break: auto-phrase`) で折り返す。行の長さを揃えるので最後の行に 1-2 文字だけが残らず、ホストの文書の `lang` が `ja` なら日本語を文節の境で折り返す (171 px の列で 「プレビューを表示できませ / ん」 が 「プレビューを / 表示できません」 になる)。`auto-phrase` を持たない Firefox と Safari は宣言を捨てて今までどおりに折り返し、`balance` を持たない Safari 17.4 は行の長さを揃えない (README の Browser floor)。
28
+ - **開発者向け**: `pnpm run verify` の単体テストは、リポジトリーの公開の漏洩検査を配布する Markdown (`package.json` の `files` が配る `README.md`・`CHANGELOG.md`) へ検査自身の規則で当てる (`src/shipped-markdown-leaks.spec.ts`)。内部の名前は公開の段ではなく検証の段で落ちる。
29
+ `StreamEntry` と描画ポートの契約の定数 (`DAILY_REPORT_ATTACHMENT_THUMBNAIL_OUTPUT_MEDIA_TYPES`・`DAILY_REPORT_ATTACHMENT_THUMBNAIL_MAX_OUTPUT_BYTES`) はポートのモジュール (`src/server/ports.ts`) に置き、ポートは他のサーバーのモジュールを読み込まない (型の循環を断った。公開の名前と入口は変わらない)。
30
+ DetailList の行の spec は、本物の VirtualScroll の上で、差の和が 0 の測り直しの組 (1 回の ResizeObserver の配達で行 2 つが +40 と −40) の後も行の枠がそれぞれの実測の高さに置き直され、間の行だけが動き、錨が変わらないことを確かめる。直すのは `@aiquants/virtualscroll` の次の版 (CHANGELOG の 3.11.0) で、spec はワークスペースの virtualscroll のソースで走る。ピアの下限はその版の公開と一緒に上げる。
31
+
32
+ ### Breaking
33
+
34
+ - ピアの `@aiquants/virtualscroll` の下限を 3.11.0 へ上げた (`peer-floors.json`)。テスト用のハンドルの `DailyReportRevealOptions` は 3.11.0 の `scrollToIndex` の引数 (`align: "nearest"` を含む) で、DetailList の行が差の和が 0 の測り直しの組の後も木の上端と高さに戻るのは 3.11.0 の行の memo が木の版に従うため。
35
+ Migration: `@aiquants/virtualscroll` を 3.11.0 以降へ上げる。
36
+
37
+ ## 0.27.0 (2026-10-05)
38
+
39
+ 0.26.0 の続き: Fetch Metadata による要求の隔離、ポインター専用のスクロールバー (名前の無い `scrollbar` と `slider` を出さない)、添付の格子の整数 px の列、要求が全員去った描画の結果の保持、SSE リーダーのポート、E2E のテスト用ハンドルの読み取り専用化。どれも README の該当の節が今の契約を記す。
40
+
41
+ ### Security
42
+
43
+ - **別オリジンのページからの要求は認証より前に 403**: API (`api.loader`・`api.action`)・SSE (`sse.loader`)・添付 (`attachment.loader`) のどの経路も、最初の文で要求の Fetch Metadata を見て (`isCrossSiteRequest`、`src/server/request-isolation.ts`)、`Sec-Fetch-Site` が `same-origin` でも `none` でもない要求 (同じサイトの兄弟のサブドメインの `same-site` を含む) を、認証・レートの課金・可視集合の解決・認可・サービスの呼び出しのどれより前に 403 (`{"error":{"message":"Cross-site request rejected"}}`・`Cache-Control: no-store`・`X-Content-Type-Options: nosniff`。`Set-Cookie` もログの行も無い) で断る。
44
+ 断らないのは、最上位のナビゲーション (`Sec-Fetch-Mode: navigate`・GET か HEAD・`Sec-Fetch-Dest` が `object` でも `embed` でもない。別のページのリンクから原本を開く) と、`Sec-Fetch-Site` の無い要求 (ブラウザーでないクライアント。下限のどのブラウザーも送る) だけ。添付の 403 は添付の失敗の組み立てを通るので、添付のどの応答とも同じヘッダー (`Cross-Origin-Resource-Policy: same-origin`・`nosniff`) を持つ。応答の側の `Cross-Origin-Resource-Policy` は多重の守りとして残す (README の Request isolation)。
45
+ 以前は、セッションの Cookie (通常の `SameSite=Lax`) が付く同じサイトの別オリジンのページから、フォームの POST が日報の作成・更新・公開・削除・コメント・スター・既読へ届き (フォームの送信はプリフライトを要さず、React Router はリソースの経路に CSRF の検査をかけない)、`<img>`・no-cors の `fetch`・HEAD の探りが認証・レートのトークン・可視集合の解決・認可まで走らせた (見える添付と見えない添付で応答までの仕事が違い、閲覧者のレートのバケットを空にして 429 にできた)。`createDailyReportServer` か `createDailyReportHandlers` を結線したホストは、この版へ上げるだけで守られる。ページと別のオリジンから API を呼ぶホストは断られる (パッケージのクライアントはページ自身のオリジンからしか呼ばない)。
46
+
47
+ ### Changed
48
+
49
+ - **スクロールバーはポインター専用 (名前の無い `scrollbar` と `slider` を出さない)**: 両ビューが渡す `enableArrowButtonTabStops: false` は、`@aiquants/virtualscroll` 3.10.0 で「ホストがキーボードのスクロールを自分で持つ」ことのただ 1 つの合図になり、バーはネイティブのスクロールバーと同じポインター専用の部品になる。バー全体が `aria-hidden="true"` なので支援技術には (名前の無いものも含め) `scrollbar` も `slider` も見えず、矢印は Tab 順に入らず、バーのどこを押しても既定動作を取り消してフォーカスを動かさない。ポインターのスクロール (矢印と長押し・溝・つまみ・タップスクロールサークル) は変わらない。
50
+ 以前は名前の無い `role="scrollbar"` と `role="slider"` が支援技術に見え (WCAG 4.1.2)、バーの押下は行にあったフォーカスをバー (`tabIndex={-1}`) へ移した。ラベルのカタログは virtualscroll の 11 個のエンジンキーを再利用し、新しい 4 キー (`verticalScrollBar`・`horizontalScrollBar`・`verticalScrollThumb`・`horizontalScrollThumb`) がバーとつまみの名前になる (隠れたバーの名前も `config.labels` で上書きできる)。キーは全 94 (エンジン 11 + 固有 83)。
51
+ - **添付の格子の列は整数 px**: 格子は自分の大きさの容器 (`data-testid="daily-report-attachment-grid-container"`。inline-size の容器で、格子の包含ブロック) の中に 1 つだけ置き、容器の内容の幅 W が列数・格子の幅・ファイル名を収める列幅のただ 1 つの入力になる。n 列の格子の幅は列幅 t が整数 px になる最も広い n·t + 16(n − 1) ≤ W (区切りごとに列数の隣に書く `round()` の幅。列は `1fr` のまま) で、列幅は t = ⌊(min(W, 1,264) − 16(n − 1)) / n⌋、隙間はどれもちょうど 16px、余り (W が整数 px なら 0〜n − 1px) は格子の外の終わりの側に残る。
52
+ 1280 × 800 の側面ペインの列幅は 234⅔ と 231⅓ から 234 と 231 になる (枠の高さ 156 と 154 は変わらない)。以前の端数の列幅はタイルの縁を画素の間に置き、比 1 では隙間を 16 と 17px で描いた。`round()` を持たない Chrome 121〜124 は幅の宣言だけを捨て、今までの端数の列幅で同じ列数に並ぶ。添付の節は大きさの容器でなくなり、ファイル名を収める幅の観測器は格子ではなく格子の容器を測る (格子そのものの幅は余りを除いた幅なので、そこからは列数が決まらない)。
53
+ - **全員が去った描画も、払い終えた結果を残す**: サムネイルの生成は、描画の締め切り (502 `render_timeout`) か、待っている要求が全員去ったとき (503 `aborted`。その時点で答える) に描画を待つのをやめ、どちらでもその描画を続いている描画の登録簿へ渡す。描画が内容で決まる結果 (出力の検証を通った縮小画像か `unsupported`) で決着したら、同一性を確かめた生成なら、誰が待っているかを問わずキャッシュに記録してから枠を返す。決着するまで、同じ内容の次の生成は展開を重ねずにそれを待つ。中断に応じる描画ポートの `failed` と例外は記録しない。
54
+ 以前は全員が去った生成の描画の結果を種別を見ずに捨てたので、タイルが描画の窓を出て戻ると、既定の 2 枠では同じ原本をもう 1 度読んで展開した (今は読み取り 1 回・描画 1 回)。
55
+ - **開発用の SSE のスイッチに名前**: `showDevControls` の SSE の切り替えのスイッチ (`role="switch"` のボタン) は、ボタン自身に `labels.devSseToggle` の `aria-label` と `title` を持つ。以前は囲む要素の `title` だけで、スイッチは名前を持たなかった (WCAG 4.1.2)。
56
+ - **強制配色のスクロールバー**: `VIEW_SCROLL_BAR_THEME_CLASS_NAME` は強制配色 (`forced-colors: active`) で、つまみを状態ごとの規則と同じ選択子でシステム色に言い直し (休み `CanvasText`、ホバーとドラッグ `Highlight`、無効 `GrayText`)、溝に内側の 1px の `CanvasText` の輪郭を付ける (配置は変わらない)。以前は強制配色でつまみも溝も `Canvas` になり、つまみが見えなかった。
57
+ - **E2E のテスト用ハンドルは読み取りと見せ方 1 つ** (Breaking を参照): `__virtualScroll` は VirtualScroll のハンドルの読み取りの部分 (`getViewportSize`・`getScrollPosition`・`getScrollAnchor`・`getRange`・`getFenwickSize`)、日報の検索 3 つ、ビューのスクロールの口を通る `revealIndex(index, options)` だけを持つ。`revealIndex` はキーの移動と同じく呼び出しの直後に錨を記録するので、直後の一覧の変化も見せた日報を同じ位置に留める。以前はハンドルを丸ごと広げたので、E2E が錨を通らずに位置を動かせた。
58
+ - **SSE リーダーはポートの型で受ける**: `createDailyReportHandlers` の設定の `sseReader` は、クラス `DailyReportSseReader` の型ではなくポート `DailyReportSseReaderPort` (`ready()` と `subscribe(onEntry, onError)`) で受ける。0.24.0 と 0.26.0 の Breaking が言う、ホストが自前で差し替えるリーダーが実装するのはこの型。以前のクラスの型は private の欄を持つので、同じ 2 つのメソッドを持つホストの実装も型の読み替えなしには渡せなかった。
59
+ - **開発者向け**: 公開のスクリプト (`publish:*`) は `pnpm run verify` が作ったビルドをビルドし直さずに測り、`node scripts/check-bundle-size.mjs --write`、続けて `--exact` を走らせる。縮んだ項目は `--write` が下げるので、縮みは公開する版の基準に作りとして記録される (以前は縮みも `--exact` で落ち、基準を記録し直すコミットが要った)。増えた項目は記録せずに `--exact` で止め、レビューを通した `--write --accept` を待つ。
60
+ 配置の格子の単位 u = 4 は `src/shared/layout-lattice.ts` の `LAYOUT_LATTICE_PX` ただ 1 つで、List の行の枠・DetailList の測る前の行の見積もり (88u = 352)・添付のタイルの寄せはどれもそこから読む (写しの宣言は `constants.spec.ts` が構文木で落とす)。SQL の結果のキャッシュのキーはどれも `cache-keys.ts` の組み立てが可視集合のダイジェストで閉じ、ID 一覧の名前空間の接頭辞は `DAILY_REPORT_IDS_CACHE_PREFIX` (`"daily-report:ids:"`)、営業日の解決のキーも組み立て (`businessDateCacheKey`) から作る (キーのバイト列は変わらない)。錨の spec は本物の VirtualScroll の上でも、窓の中と外の測った行の置き換えと、下端の揃えを保留している間の窓の中の削除を確かめる。
61
+
62
+ ### Added
63
+
64
+ - server の型 `DailyReportSseReaderPort`: 共有 SSE リーダーのポート (上の Changed)。
65
+
66
+ ### Breaking
67
+
68
+ - `__virtualScroll` (両ビューのテスト用ハンドル。README の Test hooks) から、VirtualScroll のハンドルの位置を動かす関数 (`scrollToIndex`・`scrollBy`・`scrollTo`・`applyWheel`・`updateItemSize`) と、`getContentSize`・`getFenwickTreeTotalHeight`・`focusItemAtIndex` を外した。Migration: `__virtualScroll.scrollToIndex(index, options)` は `__virtualScroll.revealIndex(index, options)` へ置き換える (揃え方と offset は同じ)。ほかの位置の変化は画面の操作 (キー・ホイール・クリック) で起こす。
69
+ - ピアの `@aiquants/virtualscroll` の下限を 3.10.0 へ上げる。両ビューは 3.10.0 の `enableArrowButtonTabStops: false` (ポインター専用のバー) と、バーとつまみの名前のキーに頼る (3.9.0 では名前の無い `scrollbar` と `slider` が支援技術に見え、バーの押下がフォーカスを移す)。Migration: `@aiquants/virtualscroll` を 3.10.0 以降へ上げる。`labels` で上書きできるエンジンのキーは 4 つ増える (上書きは任意)。
70
+
5
71
  ## 0.26.0 (2026-10-04)
6
72
 
7
73
  0.25.0 の続き: 位置の変化ごとの錨の記録、行と面の 4 px の格子、読み込みの画面の枠とエラーの文言の色、コメントの節の 1 つの規則、添付の配信とサムネイルの生成の締め付け、共有 SSE リーダーの位置の固定。どれも README の該当の節が今の契約を記す。
@@ -44,7 +110,7 @@ All notable changes to `@aiquants/daily-report` are documented here.
44
110
 
45
111
  ### Breaking
46
112
 
47
- - `DailyReportSseReader.ready()` はストリームの末尾を読めないとき拒否する (0.24.0 と 0.25.0 は `"$"` へ縮退して解決した)。Migration: 自前の SSE ハンドラで共有リーダーを使うホストは、`ready()` の拒否で接続を閉じてクライアントに開き直させる (同梱のハンドラはそうしている)。リーダーを自前で差し替えるホストの `ready()` も、位置を具体 ID に固定できないときは拒否する。
113
+ - `DailyReportSseReader.ready()` はストリームの末尾を読めないとき拒否する (0.24.0 と 0.25.0 は `"$"` へ縮退して解決した)。Migration: 自前の SSE ハンドラで共有リーダーを使うホストは、`ready()` の拒否で接続を閉じてクライアントに開き直させる (同梱のハンドラはそうしている)。リーダーを自前で差し替えるホストの `ready()` も、位置を具体 ID に固定できないときは拒否する (自前のリーダーが実装する型は、後の版が公開した `DailyReportSseReaderPort`)。
48
114
  - ピアの `@aiquants/virtualscroll` の下限を 3.9.0 へ上げる。両ビューは 3.9.0 の `onScrollAdjust` で錨を記録し直し、揃えた端を守る装置の画素への揃えに頼る。Migration: `@aiquants/virtualscroll` を 3.9.0 以降へ上げる。
49
115
  - server の `SqlResultCache` から `invalidate` と `flush` を外した。Migration: 1 つのキーを消していた呼び出しは、そのキーを接頭辞とする `invalidatePrefix(<キー>)` か、全部を消す `clearAll()` へ置き換える。
50
116
 
@@ -126,7 +192,7 @@ All notable changes to `@aiquants/daily-report` are documented here.
126
192
  - **`subscribe` の `onError` が必須に**: `DailyReportSseReader.subscribe(onEntry, onError)` の `onError` は省略できない。run の失敗は内部の `"error"` イベントでは
127
193
  なく各購読者の `onError` へだけ届き、呼ばれた時点で購読は解除済みなので、呼び出し側は接続を閉じてクライアントに開き直させる (同梱の SSE ハンドラはそうしている)。
128
194
  購読者が残っていても失敗した run は再起動せず、次の購読が新しい run を起動する。購読より前に読んだエントリは再生しない。`destroy()` は全購読者を通知なしで
129
- 外して run を止め、読み取り位置を初期化する (実行中の `subscribe` は例外になる)。リーダーを自前で差し替えるホストは `ready()` も実装する。
195
+ 外して run を止め、読み取り位置を初期化する (実行中の `subscribe` は例外になる)。リーダーを自前で差し替えるホストは `ready()` も実装する (実装する型は、後の版が公開した `DailyReportSseReaderPort`)。
130
196
 
131
197
  ## 0.23.0 (2026-10-03)
132
198