@aiquants/daily-report 0.18.0 → 0.18.2

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.
@@ -20,12 +20,15 @@ import { DailyReportViewerScope, isSourceTypeVisible, type VisibleSourceTypeSet
20
20
 
21
21
  type LoaderArgs = { request: Request; params: Record<string, string | undefined> }
22
22
 
23
- /** `data()` 相当の JSON レスポンス生成 (react-router 非依存)。 */
24
- const jsonData = (payload: unknown, init?: { status?: number }): Response =>
25
- new Response(JSON.stringify(payload), {
26
- status: init?.status ?? 200,
27
- headers: { "Content-Type": "application/json" },
28
- })
23
+ /** `data()` 相当の JSON レスポンス生成 (react-router 非依存)。`setCookie` 指定時は Set-Cookie を付与する。 */
24
+ const jsonData = (payload: unknown, init?: { status?: number; setCookie?: string }): Response => {
25
+ const headers = new Headers({ "Content-Type": "application/json" })
26
+ // セッション破棄の Set-Cookie は未認証応答にも載せる (捨てると無効セッションがブラウザに残る)
27
+ if (init?.setCookie) {
28
+ headers.append("Set-Cookie", init.setCookie)
29
+ }
30
+ return new Response(JSON.stringify(payload), { status: init?.status ?? 200, headers })
31
+ }
29
32
 
30
33
  // ---------------- ids NDJSON ストリームの定数とヘルパー (純粋・モジュールスコープ) ----------------
31
34
 
@@ -719,10 +722,10 @@ export function createDailyReportHandlers(config: DailyReportHandlersConfig) {
719
722
  headers.append("Set-Cookie", cookie)
720
723
  }
721
724
 
722
- let userId: number | null = null
723
- if (user) {
724
- userId = await service.getUserIdByExternalId(user.id)
725
- }
725
+ // 文字列モード (`failureRedirect: loginRedirectPath`) では認証失敗が redirect として
726
+ // throw されるため `user` は必ず存在する (`DailyReportAuthenticate` のオーバーロードも
727
+ // そう宣言している)。`if (user)` は到達しないので置かない
728
+ const userId: number | null = await service.getUserIdByExternalId(user.id)
726
729
 
727
730
  const hashedUserId = userId ? encodeUserId(userId) : null
728
731
 
@@ -943,199 +946,229 @@ export function createDailyReportHandlers(config: DailyReportHandlersConfig) {
943
946
  * 日報データの変更操作を処理するアクション。
944
947
  */
945
948
  const apiAction = async ({ request, params }: LoaderArgs) => {
946
- const { user } = await authenticate(request, { failureRedirect: null })
947
- if (!user) {
948
- return jsonData({ error: "Unauthorized" }, { status: 401 })
949
- }
949
+ const { user, cookie } = await authenticate(request, { failureRedirect: null })
950
950
 
951
- const endpoint = params.endpoint
952
- if (endpoint !== "action") {
953
- return jsonData({ error: "Unknown endpoint" }, { status: 404 })
954
- }
951
+ /**
952
+ * Builds a JSON response that always carries the session cookie for this request.
953
+ * 本リクエストのセッション cookie を必ず載せた JSON レスポンスの生成。
954
+ *
955
+ * 全分岐 (成功・4xx・未認証) で転送する必要がある。トークン更新の cookie を落とすとブラウザは
956
+ * 期限切れトークンを持ち続け、以後のリクエストが毎回リフレッシュを試みて収束しない。
957
+ * 破棄 cookie を落とせば無効セッションがブラウザに残り続ける。個別の `jsonData` へ付け回すと
958
+ * 必ず取りこぼすため、この関数以外を本ハンドラ内で使わない。
959
+ *
960
+ * @param payload Response payload / レスポンス本文
961
+ * @param init Status override / ステータス指定
962
+ * @returns JSON response carrying the session cookie / セッション cookie を載せた JSON レスポンス
963
+ */
964
+ const respond = (payload: unknown, init?: { status?: number }): Response => jsonData(payload, { ...init, setCookie: cookie })
955
965
 
956
- const userId = await service.getUserIdByExternalId(user.id)
957
- if (!userId) {
958
- return jsonData({ error: "User not found" }, { status: 404 })
966
+ if (!user) {
967
+ return respond({ error: "Unauthorized" }, { status: 401 })
959
968
  }
960
969
 
961
- // 可視集合はリクエストごとに 1 回だけ解決し、他者の日報に作用する 4 種へ渡す
962
- // (create / update / publish / delete / clearCache は所有者・開発者ゲートで別管理)
963
- const scope = await resolveViewerScope(request)
964
-
965
- const formData = await request.formData()
966
- const intent = formData.get("intent")
967
- const reportHubIdRaw = formData.get("reportHubId")
968
- const reportHubId = reportHubIdRaw ? Number(reportHubIdRaw) : NaN
969
- const businessDate = formData.get("businessDate") as string | null
970
- const operationTimestamp = Number(formData.get("operationTimestamp"))
971
- const clientTempId = formData.get("clientTempId") as string | null
972
-
973
- if (intent === "clearCache") {
974
- // 開発専用アフォーダンス。既定は封鎖 (認証済みなら誰でも全ワーカーの
975
- // キャッシュを無制限破棄できてしまうため、注入で明示的に開けた環境のみ許可)
976
- if (!config.enableDevCacheClear) {
977
- return jsonData({ error: "Invalid intent" }, { status: 400 })
970
+ // apiAction には外側 try/catch が無く、`respond` を通らない例外 (service / DB / Redis の
971
+ // 失敗) がハンドラの外へ抜けていた。抜けるとセッション cookie が載らないまま 500 になり、
972
+ // トークン更新がブラウザへ届かない。apiLoader (:930) と同じ形で包む
973
+ const actionStartTime = Date.now()
974
+ try {
975
+ const endpoint = params.endpoint
976
+ if (endpoint !== "action") {
977
+ return respond({ error: "Unknown endpoint" }, { status: 404 })
978
978
  }
979
- await service.clearCache()
980
- return jsonData({ status: "OK", intent: "clearCache" })
981
- }
982
979
 
983
- if (!clientTempId) {
984
- return jsonData({ error: "clientTempId required" }, { status: 400 })
985
- }
980
+ const userId = await service.getUserIdByExternalId(user.id)
981
+ if (!userId) {
982
+ return respond({ error: "User not found" }, { status: 404 })
983
+ }
986
984
 
987
- if (intent !== "create" && (!reportHubId || Number.isNaN(reportHubId))) {
988
- return jsonData({ error: "Invalid reportHubId" }, { status: 400 })
989
- }
985
+ // 可視集合はリクエストごとに 1 回だけ解決し、他者の日報に作用する 4 種へ渡す
986
+ // (create / update / publish / delete / clearCache は所有者・開発者ゲートで別管理)
987
+ const scope = await resolveViewerScope(request)
990
988
 
991
- /**
992
- * Runs a boolean user-status toggle and maps its outcome onto a response.
993
- * 真偽値のユーザー状態トグルを実行し、結果をレスポンスへ写像する処理。
994
- *
995
- * `toggleStar` と `toggleRead` は必須フィールド名と呼び出すサービスだけが異なるため、
996
- * 検証・応答整形・エラー写像をここへ集約する。
997
- *
998
- * @param intentName Intent echoed back to the client. クライアントへ返す intent 名。
999
- * @param fieldName Required boolean form field. 必須の真偽値フォームフィールド名。
1000
- * @param apply Service call performing the toggle. トグルを実行するサービス呼び出し。
1001
- * @returns The HTTP response. HTTP レスポンス。
1002
- */
1003
- const runStatusToggle = async (intentName: "toggleStar" | "toggleRead", fieldName: "isStarred" | "isRead", apply: (value: boolean) => Promise<{ isStarred: boolean; isRead: boolean }>): Promise<Response> => {
1004
- const raw = formData.get(fieldName)
1005
- if (raw === null) {
1006
- return jsonData({ error: `${fieldName} required` }, { status: 400 })
1007
- }
1008
- try {
1009
- const updatedStatus = await apply(raw === "true")
1010
- // クライアントは isStarred/isRead のみ参照する。生内部 ID (userId/created_by/updated_by) を
1011
- // 含む行全体は返さず、必要なフラグだけに絞る (内部 ID の自己開示防止)。
1012
- return jsonData({ status: "OK", intent: intentName, updatedStatus: { isStarred: updatedStatus.isStarred, isRead: updatedStatus.isRead }, operationTimestamp, reportHubId: String(reportHubId), clientTempId })
1013
- } catch (e: unknown) {
1014
- // 不可視区分・不存在・削除済みは 404 へ写像する (未捕捉だと 500 になる)
1015
- if (e instanceof Error && e.message === "Not Found") {
1016
- return jsonData({ error: "Report not found" }, { status: 404 })
989
+ const formData = await request.formData()
990
+ const intent = formData.get("intent")
991
+ const reportHubIdRaw = formData.get("reportHubId")
992
+ const reportHubId = reportHubIdRaw ? Number(reportHubIdRaw) : NaN
993
+ const businessDate = formData.get("businessDate") as string | null
994
+ const operationTimestamp = Number(formData.get("operationTimestamp"))
995
+ const clientTempId = formData.get("clientTempId") as string | null
996
+
997
+ if (intent === "clearCache") {
998
+ // 開発専用アフォーダンス。既定は封鎖 (認証済みなら誰でも全ワーカーの
999
+ // キャッシュを無制限破棄できてしまうため、注入で明示的に開けた環境のみ許可)
1000
+ if (!config.enableDevCacheClear) {
1001
+ return respond({ error: "Invalid intent" }, { status: 400 })
1017
1002
  }
1018
- throw e
1003
+ await service.clearCache()
1004
+ return respond({ status: "OK", intent: "clearCache" })
1019
1005
  }
1020
- }
1021
1006
 
1022
- switch (intent) {
1023
- case "create": {
1024
- if (!businessDate) {
1025
- return jsonData({ error: "businessDate required" }, { status: 400 })
1026
- }
1027
- const newReport = await service.createDailyReport(userId, businessDate, clientTempId)
1028
- return jsonData({
1029
- status: "OK",
1030
- intent: "create",
1031
- report: newReport,
1032
- reportHubId: String(newReport.reportHubId),
1033
- clientTempId, // Echo back for validation
1034
- })
1007
+ if (!clientTempId) {
1008
+ return respond({ error: "clientTempId required" }, { status: 400 })
1035
1009
  }
1036
- case "update": {
1037
- const title = formData.get("title") as string | undefined
1038
- const content = formData.get("content") as string | undefined
1039
- try {
1040
- await service.updateDailyReport(reportHubId, userId, { title, content }, clientTempId)
1041
- return jsonData({ status: "OK", intent: "update", reportHubId: String(reportHubId), clientTempId })
1042
- } catch (e: unknown) {
1043
- if (e instanceof Error) {
1044
- if (e.message === "Unauthorized") {
1045
- return jsonData({ error: "Unauthorized" }, { status: 403 })
1046
- }
1047
- if (e.message === "Not Found") {
1048
- return jsonData({ error: "Report not found" }, { status: 404 })
1049
- }
1050
- }
1051
- throw e
1052
- }
1010
+
1011
+ if (intent !== "create" && (!reportHubId || Number.isNaN(reportHubId))) {
1012
+ return respond({ error: "Invalid reportHubId" }, { status: 400 })
1053
1013
  }
1054
- case "publish": {
1014
+
1015
+ /**
1016
+ * Runs a boolean user-status toggle and maps its outcome onto a response.
1017
+ * 真偽値のユーザー状態トグルを実行し、結果をレスポンスへ写像する処理。
1018
+ *
1019
+ * `toggleStar` と `toggleRead` は必須フィールド名と呼び出すサービスだけが異なるため、
1020
+ * 検証・応答整形・エラー写像をここへ集約する。
1021
+ *
1022
+ * @param intentName Intent echoed back to the client. クライアントへ返す intent 名。
1023
+ * @param fieldName Required boolean form field. 必須の真偽値フォームフィールド名。
1024
+ * @param apply Service call performing the toggle. トグルを実行するサービス呼び出し。
1025
+ * @returns The HTTP response. HTTP レスポンス。
1026
+ */
1027
+ const runStatusToggle = async (intentName: "toggleStar" | "toggleRead", fieldName: "isStarred" | "isRead", apply: (value: boolean) => Promise<{ isStarred: boolean; isRead: boolean }>): Promise<Response> => {
1028
+ const raw = formData.get(fieldName)
1029
+ if (raw === null) {
1030
+ return respond({ error: `${fieldName} required` }, { status: 400 })
1031
+ }
1055
1032
  try {
1056
- const publishedReport = await service.publishDailyReport(reportHubId, userId, clientTempId)
1057
- return jsonData({ status: "OK", intent: "publish", reportHubId: String(reportHubId), clientTempId, report: publishedReport })
1033
+ const updatedStatus = await apply(raw === "true")
1034
+ // クライアントは isStarred/isRead のみ参照する。生内部 ID (userId/created_by/updated_by) を
1035
+ // 含む行全体は返さず、必要なフラグだけに絞る (内部 ID の自己開示防止)。
1036
+ return respond({ status: "OK", intent: intentName, updatedStatus: { isStarred: updatedStatus.isStarred, isRead: updatedStatus.isRead }, operationTimestamp, reportHubId: String(reportHubId), clientTempId })
1058
1037
  } catch (e: unknown) {
1059
- if (e instanceof Error) {
1060
- if (e.message === "Unauthorized") {
1061
- return jsonData({ error: "Unauthorized" }, { status: 403 })
1062
- }
1063
- if (e.message === "Not Found") {
1064
- return jsonData({ error: "Report not found" }, { status: 404 })
1065
- }
1038
+ // 不可視区分・不存在・削除済みは 404 へ写像する (未捕捉だと 500 になる)
1039
+ if (e instanceof Error && e.message === "Not Found") {
1040
+ return respond({ error: "Report not found" }, { status: 404 })
1066
1041
  }
1067
1042
  throw e
1068
1043
  }
1069
1044
  }
1070
- case "delete": {
1071
- try {
1072
- await service.deleteDailyReport(reportHubId, userId, clientTempId)
1073
- return jsonData({
1045
+
1046
+ switch (intent) {
1047
+ case "create": {
1048
+ if (!businessDate) {
1049
+ return respond({ error: "businessDate required" }, { status: 400 })
1050
+ }
1051
+ const newReport = await service.createDailyReport(userId, businessDate, clientTempId)
1052
+ return respond({
1074
1053
  status: "OK",
1075
- intent: "delete",
1076
- reportHubId: String(reportHubId),
1077
- clientTempId, // Echo back for validation even on delete
1054
+ intent: "create",
1055
+ report: newReport,
1056
+ reportHubId: String(newReport.reportHubId),
1057
+ clientTempId, // Echo back for validation
1078
1058
  })
1079
- } catch (e: unknown) {
1080
- if (e instanceof Error) {
1081
- if (e.message === "Unauthorized") {
1082
- return jsonData({ error: "Unauthorized" }, { status: 403 })
1083
- }
1084
- if (e.message === "Not Found") {
1085
- return jsonData({ error: "Report not found" }, { status: 404 })
1059
+ }
1060
+ case "update": {
1061
+ const title = formData.get("title") as string | undefined
1062
+ const content = formData.get("content") as string | undefined
1063
+ try {
1064
+ await service.updateDailyReport(reportHubId, userId, { title, content }, clientTempId)
1065
+ return respond({ status: "OK", intent: "update", reportHubId: String(reportHubId), clientTempId })
1066
+ } catch (e: unknown) {
1067
+ if (e instanceof Error) {
1068
+ if (e.message === "Unauthorized") {
1069
+ return respond({ error: "Unauthorized" }, { status: 403 })
1070
+ }
1071
+ if (e.message === "Not Found") {
1072
+ return respond({ error: "Report not found" }, { status: 404 })
1073
+ }
1086
1074
  }
1075
+ throw e
1087
1076
  }
1088
- throw e
1089
1077
  }
1090
- }
1091
- case "toggleStar":
1092
- return await runStatusToggle("toggleStar", "isStarred", (value) => service.setStarStatus(scope, userId, reportHubId, businessDate, value, clientTempId))
1093
- case "toggleRead":
1094
- return await runStatusToggle("toggleRead", "isRead", (value) => service.setReadStatus(scope, userId, reportHubId, businessDate, value, clientTempId))
1095
- case "addComment": {
1096
- const content = formData.get("content") as string
1097
- if (!content) {
1098
- return jsonData({ error: "Content required" }, { status: 400 })
1099
- }
1100
- try {
1101
- const newComment = await service.addComment(scope, userId, reportHubId, content, businessDate, clientTempId)
1102
- const safeComment = { ...newComment, userId: newComment.userId }
1103
- return jsonData({ status: "OK", intent: "addComment", newComment: safeComment, reportHubId: String(reportHubId), clientTempId })
1104
- } catch (e: unknown) {
1105
- if (e instanceof Error) {
1106
- // 削除直後の日報への投稿レース等。500 ではなく意味のある状態コードへ写像する
1107
- if (e.message === "Not Found") {
1108
- return jsonData({ error: "Report not found" }, { status: 404 })
1109
- }
1110
- if (e.message.startsWith("Comments are restricted")) {
1111
- return jsonData({ error: "Comments are restricted for this report" }, { status: 403 })
1078
+ case "publish": {
1079
+ try {
1080
+ const publishedReport = await service.publishDailyReport(reportHubId, userId, clientTempId)
1081
+ return respond({ status: "OK", intent: "publish", reportHubId: String(reportHubId), clientTempId, report: publishedReport })
1082
+ } catch (e: unknown) {
1083
+ if (e instanceof Error) {
1084
+ if (e.message === "Unauthorized") {
1085
+ return respond({ error: "Unauthorized" }, { status: 403 })
1086
+ }
1087
+ if (e.message === "Not Found") {
1088
+ return respond({ error: "Report not found" }, { status: 404 })
1089
+ }
1112
1090
  }
1091
+ throw e
1113
1092
  }
1114
- throw e
1115
1093
  }
1116
- }
1117
- case "deleteComment": {
1118
- const commentId = Number(formData.get("commentId"))
1119
- if (!commentId || Number.isNaN(commentId)) {
1120
- return jsonData({ error: "Invalid commentId" }, { status: 400 })
1094
+ case "delete": {
1095
+ try {
1096
+ await service.deleteDailyReport(reportHubId, userId, clientTempId)
1097
+ return respond({
1098
+ status: "OK",
1099
+ intent: "delete",
1100
+ reportHubId: String(reportHubId),
1101
+ clientTempId, // Echo back for validation even on delete
1102
+ })
1103
+ } catch (e: unknown) {
1104
+ if (e instanceof Error) {
1105
+ if (e.message === "Unauthorized") {
1106
+ return respond({ error: "Unauthorized" }, { status: 403 })
1107
+ }
1108
+ if (e.message === "Not Found") {
1109
+ return respond({ error: "Report not found" }, { status: 404 })
1110
+ }
1111
+ }
1112
+ throw e
1113
+ }
1121
1114
  }
1122
- try {
1123
- await service.deleteComment(scope, userId, reportHubId, commentId, businessDate, clientTempId)
1124
- return jsonData({ status: "OK", reportHubId: String(reportHubId), deletedCommentId: String(commentId), clientTempId })
1125
- } catch (e: unknown) {
1126
- if (e instanceof Error) {
1127
- if (e.message === "Unauthorized") {
1128
- return jsonData({ error: "Unauthorized" }, { status: 403 })
1115
+ case "toggleStar":
1116
+ return await runStatusToggle("toggleStar", "isStarred", (value) => service.setStarStatus(scope, userId, reportHubId, businessDate, value, clientTempId))
1117
+ case "toggleRead":
1118
+ return await runStatusToggle("toggleRead", "isRead", (value) => service.setReadStatus(scope, userId, reportHubId, businessDate, value, clientTempId))
1119
+ case "addComment": {
1120
+ const content = formData.get("content") as string
1121
+ if (!content) {
1122
+ return respond({ error: "Content required" }, { status: 400 })
1123
+ }
1124
+ try {
1125
+ const newComment = await service.addComment(scope, userId, reportHubId, content, businessDate, clientTempId)
1126
+ const safeComment = { ...newComment, userId: newComment.userId }
1127
+ return respond({ status: "OK", intent: "addComment", newComment: safeComment, reportHubId: String(reportHubId), clientTempId })
1128
+ } catch (e: unknown) {
1129
+ if (e instanceof Error) {
1130
+ // 削除直後の日報への投稿レース等。500 ではなく意味のある状態コードへ写像する
1131
+ if (e.message === "Not Found") {
1132
+ return respond({ error: "Report not found" }, { status: 404 })
1133
+ }
1134
+ if (e.message.startsWith("Comments are restricted")) {
1135
+ return respond({ error: "Comments are restricted for this report" }, { status: 403 })
1136
+ }
1129
1137
  }
1130
- if (e.message === "Not Found") {
1131
- return jsonData({ error: "Comment not found" }, { status: 404 })
1138
+ throw e
1139
+ }
1140
+ }
1141
+ case "deleteComment": {
1142
+ const commentId = Number(formData.get("commentId"))
1143
+ if (!commentId || Number.isNaN(commentId)) {
1144
+ return respond({ error: "Invalid commentId" }, { status: 400 })
1145
+ }
1146
+ try {
1147
+ await service.deleteComment(scope, userId, reportHubId, commentId, businessDate, clientTempId)
1148
+ return respond({ status: "OK", reportHubId: String(reportHubId), deletedCommentId: String(commentId), clientTempId })
1149
+ } catch (e: unknown) {
1150
+ if (e instanceof Error) {
1151
+ if (e.message === "Unauthorized") {
1152
+ return respond({ error: "Unauthorized" }, { status: 403 })
1153
+ }
1154
+ if (e.message === "Not Found") {
1155
+ return respond({ error: "Comment not found" }, { status: 404 })
1156
+ }
1132
1157
  }
1158
+ throw e
1133
1159
  }
1134
- throw e
1135
1160
  }
1161
+ default:
1162
+ return respond({ error: "Invalid intent" }, { status: 400 })
1136
1163
  }
1137
- default:
1138
- return jsonData({ error: "Invalid intent" }, { status: 400 })
1164
+ } catch (error) {
1165
+ // React Router のリダイレクト等は握り潰さず素通しする
1166
+ if (error instanceof Response) throw error
1167
+ const elapsed = Date.now() - actionStartTime
1168
+ const err = error instanceof Error ? error : new Error(String(error))
1169
+ apiLogger.error(`500 action elapsed=${elapsed}ms name=${err.name} message=${err.message}`)
1170
+ apiLogger.error("Stack:", err.stack)
1171
+ return respond({ error: "Internal Server Error" }, { status: 500 })
1139
1172
  }
1140
1173
  }
1141
1174
 
@@ -1147,20 +1180,29 @@ export function createDailyReportHandlers(config: DailyReportHandlersConfig) {
1147
1180
  */
1148
1181
  const sseLoader = async ({ request, params }: LoaderArgs) => {
1149
1182
  // 認証チェック
1150
- const { user } = await authenticate(request, { failureRedirect: null })
1183
+ const { user, cookie } = await authenticate(request, { failureRedirect: null })
1184
+ // セッション cookie は**この応答がどれになろうと**転送する。トークンが更新された
1185
+ // リクエストがたまたま SSE 接続だった場合、ここで落とすと更新後のトークンがブラウザへ
1186
+ // 届かず、以降のリクエストが毎回リフレッシュをやり直すことになる。破棄 cookie を
1187
+ // 落とした場合はさらに悪く、無効セッションがブラウザに残り続ける (apiAction と同じ理由)。
1188
+ // SSE は接続が長命なぶん、更新を担うリクエストになりやすい
1189
+ const sessionHeaders = (extra?: Record<string, string>): Record<string, string> | undefined => {
1190
+ if (cookie === undefined) return extra
1191
+ return { ...(extra ?? {}), "Set-Cookie": cookie }
1192
+ }
1151
1193
  if (!user) {
1152
- return new Response("Unauthorized", { status: 401 })
1194
+ return new Response("Unauthorized", { status: 401, headers: sessionHeaders() })
1153
1195
  }
1154
1196
 
1155
1197
  // 内部ユーザー ID の解決
1156
1198
  const internalUserId = await service.getUserIdByExternalId(user.id)
1157
1199
  if (!internalUserId) {
1158
- return new Response("Forbidden", { status: 403 })
1200
+ return new Response("Forbidden", { status: 403, headers: sessionHeaders() })
1159
1201
  }
1160
1202
 
1161
1203
  // エンドポイントの検証
1162
1204
  if (params.endpoint !== "updates") {
1163
- return new Response("Not Found", { status: 404 })
1205
+ return new Response("Not Found", { status: 404, headers: sessionHeaders() })
1164
1206
  }
1165
1207
 
1166
1208
  // 可視集合は接続時に解決し、以降 keep-alive tick に相乗りして再解決する (D6)。
@@ -1422,11 +1464,11 @@ export function createDailyReportHandlers(config: DailyReportHandlersConfig) {
1422
1464
  })
1423
1465
 
1424
1466
  return new Response(stream, {
1425
- headers: {
1467
+ headers: sessionHeaders({
1426
1468
  "Content-Type": "text/event-stream",
1427
1469
  "Cache-Control": "no-cache, no-transform",
1428
1470
  Connection: "keep-alive",
1429
- },
1471
+ }),
1430
1472
  })
1431
1473
  }
1432
1474