@render-harness/ui 0.1.1 → 0.1.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.
Files changed (106) hide show
  1. package/dist/index.d.ts +6 -6
  2. package/dist/index.js +25 -14
  3. package/dist/index.js.map +1 -1
  4. package/dist/static/assets/{architectureDiagram-3BPJPVTR-D4589TUy.js → architectureDiagram-3BPJPVTR-DLNRxqTW.js} +2 -2
  5. package/dist/static/assets/{architectureDiagram-3BPJPVTR-D4589TUy.js.map → architectureDiagram-3BPJPVTR-DLNRxqTW.js.map} +1 -1
  6. package/dist/static/assets/{blockDiagram-GPEHLZMM-Cfjv0TRV.js → blockDiagram-GPEHLZMM-CCWTkWe6.js} +2 -2
  7. package/dist/static/assets/{blockDiagram-GPEHLZMM-Cfjv0TRV.js.map → blockDiagram-GPEHLZMM-CCWTkWe6.js.map} +1 -1
  8. package/dist/static/assets/{c4Diagram-AAUBKEIU-BoXkzmRI.js → c4Diagram-AAUBKEIU-qO0tZRXO.js} +2 -2
  9. package/dist/static/assets/{c4Diagram-AAUBKEIU-BoXkzmRI.js.map → c4Diagram-AAUBKEIU-qO0tZRXO.js.map} +1 -1
  10. package/dist/static/assets/channel-CAoVqDgM.js +2 -0
  11. package/dist/static/assets/{channel-DusP3IMn.js.map → channel-CAoVqDgM.js.map} +1 -1
  12. package/dist/static/assets/{chunk-2J33WTMH-Lf1eCW8w.js → chunk-2J33WTMH-DaEvWRZX.js} +2 -2
  13. package/dist/static/assets/{chunk-2J33WTMH-Lf1eCW8w.js.map → chunk-2J33WTMH-DaEvWRZX.js.map} +1 -1
  14. package/dist/static/assets/{chunk-3OPIFGDE-BYpEG0v0.js → chunk-3OPIFGDE-xcRWDOrg.js} +2 -2
  15. package/dist/static/assets/{chunk-3OPIFGDE-BYpEG0v0.js.map → chunk-3OPIFGDE-xcRWDOrg.js.map} +1 -1
  16. package/dist/static/assets/{chunk-5ZQYHXKU-CxDIhrF-.js → chunk-5ZQYHXKU-B_3UEJ_q.js} +2 -2
  17. package/dist/static/assets/{chunk-5ZQYHXKU-CxDIhrF-.js.map → chunk-5ZQYHXKU-B_3UEJ_q.js.map} +1 -1
  18. package/dist/static/assets/{chunk-727SXJPM-BEbDzxvV.js → chunk-727SXJPM-c1bbIj6N.js} +2 -2
  19. package/dist/static/assets/{chunk-727SXJPM-BEbDzxvV.js.map → chunk-727SXJPM-c1bbIj6N.js.map} +1 -1
  20. package/dist/static/assets/{chunk-AQP2D5EJ-DGTbQ17q.js → chunk-AQP2D5EJ-Wv0w6wvH.js} +2 -2
  21. package/dist/static/assets/{chunk-AQP2D5EJ-DGTbQ17q.js.map → chunk-AQP2D5EJ-Wv0w6wvH.js.map} +1 -1
  22. package/dist/static/assets/{chunk-CSCIHK7Q-Bo3glXo1.js → chunk-CSCIHK7Q-BBzr5U3o.js} +3 -3
  23. package/dist/static/assets/{chunk-CSCIHK7Q-Bo3glXo1.js.map → chunk-CSCIHK7Q-BBzr5U3o.js.map} +1 -1
  24. package/dist/static/assets/{chunk-KSCS5N6A-DVQpViur.js → chunk-KSCS5N6A-BZrceHGX.js} +2 -2
  25. package/dist/static/assets/{chunk-KSCS5N6A-DVQpViur.js.map → chunk-KSCS5N6A-BZrceHGX.js.map} +1 -1
  26. package/dist/static/assets/{chunk-L5ZTLDWV-BwOlshrN.js → chunk-L5ZTLDWV-BAcjEn3C.js} +2 -2
  27. package/dist/static/assets/{chunk-L5ZTLDWV-BwOlshrN.js.map → chunk-L5ZTLDWV-BAcjEn3C.js.map} +1 -1
  28. package/dist/static/assets/{chunk-ND2GUHAM-a2zgmbwP.js → chunk-ND2GUHAM-DHtjIa8_.js} +2 -2
  29. package/dist/static/assets/{chunk-ND2GUHAM-a2zgmbwP.js.map → chunk-ND2GUHAM-DHtjIa8_.js.map} +1 -1
  30. package/dist/static/assets/{chunk-NZK2D7GU-BbPAL6yn.js → chunk-NZK2D7GU-DuwDtSGJ.js} +2 -2
  31. package/dist/static/assets/{chunk-NZK2D7GU-BbPAL6yn.js.map → chunk-NZK2D7GU-DuwDtSGJ.js.map} +1 -1
  32. package/dist/static/assets/{chunk-O5CBEL6O-CM0slmy1.js → chunk-O5CBEL6O-KRO56hHQ.js} +2 -2
  33. package/dist/static/assets/{chunk-O5CBEL6O-CM0slmy1.js.map → chunk-O5CBEL6O-KRO56hHQ.js.map} +1 -1
  34. package/dist/static/assets/classDiagram-4FO5ZUOK-oexPF7ww.js +2 -0
  35. package/dist/static/assets/{classDiagram-4FO5ZUOK-SuK_oLpa.js.map → classDiagram-4FO5ZUOK-oexPF7ww.js.map} +1 -1
  36. package/dist/static/assets/classDiagram-v2-Q7XG4LA2-Dc4jeSLj.js +2 -0
  37. package/dist/static/assets/{classDiagram-v2-Q7XG4LA2-DMlc0uaN.js.map → classDiagram-v2-Q7XG4LA2-Dc4jeSLj.js.map} +1 -1
  38. package/dist/static/assets/{dagre-BM42HDAG-4I-9kC-8.js → dagre-BM42HDAG-au4dCVgC.js} +2 -2
  39. package/dist/static/assets/{dagre-BM42HDAG-4I-9kC-8.js.map → dagre-BM42HDAG-au4dCVgC.js.map} +1 -1
  40. package/dist/static/assets/{diagram-2AECGRRQ-pAKf5nK6.js → diagram-2AECGRRQ-rJMnDhBW.js} +2 -2
  41. package/dist/static/assets/{diagram-2AECGRRQ-pAKf5nK6.js.map → diagram-2AECGRRQ-rJMnDhBW.js.map} +1 -1
  42. package/dist/static/assets/{diagram-5GNKFQAL-CzKwZhm6.js → diagram-5GNKFQAL-diIxjA-v.js} +2 -2
  43. package/dist/static/assets/{diagram-5GNKFQAL-CzKwZhm6.js.map → diagram-5GNKFQAL-diIxjA-v.js.map} +1 -1
  44. package/dist/static/assets/{diagram-KO2AKTUF-BZNjYRzg.js → diagram-KO2AKTUF-D9IYocVR.js} +2 -2
  45. package/dist/static/assets/{diagram-KO2AKTUF-BZNjYRzg.js.map → diagram-KO2AKTUF-D9IYocVR.js.map} +1 -1
  46. package/dist/static/assets/{diagram-LMA3HP47-vnkpPjMC.js → diagram-LMA3HP47-BtdvpqaH.js} +2 -2
  47. package/dist/static/assets/{diagram-LMA3HP47-vnkpPjMC.js.map → diagram-LMA3HP47-BtdvpqaH.js.map} +1 -1
  48. package/dist/static/assets/{diagram-OG6HWLK6-CkBTEmTA.js → diagram-OG6HWLK6-CaRkxDoj.js} +2 -2
  49. package/dist/static/assets/{diagram-OG6HWLK6-CkBTEmTA.js.map → diagram-OG6HWLK6-CaRkxDoj.js.map} +1 -1
  50. package/dist/static/assets/{erDiagram-TEJ5UH35-DGrhtXAQ.js → erDiagram-TEJ5UH35-CgifrqeZ.js} +2 -2
  51. package/dist/static/assets/{erDiagram-TEJ5UH35-DGrhtXAQ.js.map → erDiagram-TEJ5UH35-CgifrqeZ.js.map} +1 -1
  52. package/dist/static/assets/{flowDiagram-I6XJVG4X-Cp2YiaJS.js → flowDiagram-I6XJVG4X-DaguIeM_.js} +2 -2
  53. package/dist/static/assets/{flowDiagram-I6XJVG4X-Cp2YiaJS.js.map → flowDiagram-I6XJVG4X-DaguIeM_.js.map} +1 -1
  54. package/dist/static/assets/{ganttDiagram-6RSMTGT7-C-jGZVXC.js → ganttDiagram-6RSMTGT7-DIFubQTa.js} +2 -2
  55. package/dist/static/assets/{ganttDiagram-6RSMTGT7-C-jGZVXC.js.map → ganttDiagram-6RSMTGT7-DIFubQTa.js.map} +1 -1
  56. package/dist/static/assets/{gitGraphDiagram-PVQCEYII-DDwOyc6i.js → gitGraphDiagram-PVQCEYII-Ht-SDjxh.js} +2 -2
  57. package/dist/static/assets/{gitGraphDiagram-PVQCEYII-DDwOyc6i.js.map → gitGraphDiagram-PVQCEYII-Ht-SDjxh.js.map} +1 -1
  58. package/dist/static/assets/index-D5tmHkO_.js +228 -0
  59. package/dist/static/assets/index-D5tmHkO_.js.map +1 -0
  60. package/dist/static/assets/index-luXiT-gK.css +1 -0
  61. package/dist/static/assets/{infoDiagram-5YYISTIA-B_rDp_NJ.js → infoDiagram-5YYISTIA-CwLGA7Zu.js} +2 -2
  62. package/dist/static/assets/{infoDiagram-5YYISTIA-B_rDp_NJ.js.map → infoDiagram-5YYISTIA-CwLGA7Zu.js.map} +1 -1
  63. package/dist/static/assets/{ishikawaDiagram-YF4QCWOH-ggQN-_CZ.js → ishikawaDiagram-YF4QCWOH-CaZCrUUR.js} +2 -2
  64. package/dist/static/assets/{ishikawaDiagram-YF4QCWOH-ggQN-_CZ.js.map → ishikawaDiagram-YF4QCWOH-CaZCrUUR.js.map} +1 -1
  65. package/dist/static/assets/{journeyDiagram-JHISSGLW-BjEC1jFt.js → journeyDiagram-JHISSGLW-C-j_GoJi.js} +2 -2
  66. package/dist/static/assets/{journeyDiagram-JHISSGLW-BjEC1jFt.js.map → journeyDiagram-JHISSGLW-C-j_GoJi.js.map} +1 -1
  67. package/dist/static/assets/{kanban-definition-UN3LZRKU-BVkPEFcm.js → kanban-definition-UN3LZRKU-DlYha85J.js} +2 -2
  68. package/dist/static/assets/{kanban-definition-UN3LZRKU-BVkPEFcm.js.map → kanban-definition-UN3LZRKU-DlYha85J.js.map} +1 -1
  69. package/dist/static/assets/{line-L2DeHwdZ.js → line-GCwXS1Fi.js} +2 -2
  70. package/dist/static/assets/{line-L2DeHwdZ.js.map → line-GCwXS1Fi.js.map} +1 -1
  71. package/dist/static/assets/mermaid-parser.core-BO_-QleW.js +5 -0
  72. package/dist/static/assets/{mermaid-parser.core-CZyyUBUW.js.map → mermaid-parser.core-BO_-QleW.js.map} +1 -1
  73. package/dist/static/assets/{mindmap-definition-RKZ34NQL-uf3ACEiM.js → mindmap-definition-RKZ34NQL-CPJIWJdQ.js} +2 -2
  74. package/dist/static/assets/{mindmap-definition-RKZ34NQL-uf3ACEiM.js.map → mindmap-definition-RKZ34NQL-CPJIWJdQ.js.map} +1 -1
  75. package/dist/static/assets/{pieDiagram-4H26LBE5-Dm2S6Ggt.js → pieDiagram-4H26LBE5-DTVmF_CH.js} +2 -2
  76. package/dist/static/assets/{pieDiagram-4H26LBE5-Dm2S6Ggt.js.map → pieDiagram-4H26LBE5-DTVmF_CH.js.map} +1 -1
  77. package/dist/static/assets/{quadrantDiagram-W4KKPZXB-CgiO6Q9Y.js → quadrantDiagram-W4KKPZXB-Bho6zy_y.js} +2 -2
  78. package/dist/static/assets/{quadrantDiagram-W4KKPZXB-CgiO6Q9Y.js.map → quadrantDiagram-W4KKPZXB-Bho6zy_y.js.map} +1 -1
  79. package/dist/static/assets/{requirementDiagram-4Y6WPE33-BFL5yKh2.js → requirementDiagram-4Y6WPE33-CLyoVwlh.js} +2 -2
  80. package/dist/static/assets/{requirementDiagram-4Y6WPE33-BFL5yKh2.js.map → requirementDiagram-4Y6WPE33-CLyoVwlh.js.map} +1 -1
  81. package/dist/static/assets/{sankeyDiagram-5OEKKPKP-DNSdTx8o.js → sankeyDiagram-5OEKKPKP-V2yvk-0Z.js} +2 -2
  82. package/dist/static/assets/{sankeyDiagram-5OEKKPKP-DNSdTx8o.js.map → sankeyDiagram-5OEKKPKP-V2yvk-0Z.js.map} +1 -1
  83. package/dist/static/assets/{sequenceDiagram-3UESZ5HK-BMNloRY9.js → sequenceDiagram-3UESZ5HK-Dp03aXVk.js} +2 -2
  84. package/dist/static/assets/{sequenceDiagram-3UESZ5HK-BMNloRY9.js.map → sequenceDiagram-3UESZ5HK-Dp03aXVk.js.map} +1 -1
  85. package/dist/static/assets/{stateDiagram-AJRCARHV-B1fkBSkt.js → stateDiagram-AJRCARHV-BEJW-_HK.js} +2 -2
  86. package/dist/static/assets/{stateDiagram-AJRCARHV-B1fkBSkt.js.map → stateDiagram-AJRCARHV-BEJW-_HK.js.map} +1 -1
  87. package/dist/static/assets/stateDiagram-v2-BHNVJYJU-Bo19-vKE.js +2 -0
  88. package/dist/static/assets/{stateDiagram-v2-BHNVJYJU-Fm1RY0qD.js.map → stateDiagram-v2-BHNVJYJU-Bo19-vKE.js.map} +1 -1
  89. package/dist/static/assets/{timeline-definition-PNZ67QCA-CdNdPzQU.js → timeline-definition-PNZ67QCA-C4KTq0mQ.js} +2 -2
  90. package/dist/static/assets/{timeline-definition-PNZ67QCA-CdNdPzQU.js.map → timeline-definition-PNZ67QCA-C4KTq0mQ.js.map} +1 -1
  91. package/dist/static/assets/{vennDiagram-CIIHVFJN-Cfr9Kji9.js → vennDiagram-CIIHVFJN-1jmBZPSZ.js} +2 -2
  92. package/dist/static/assets/{vennDiagram-CIIHVFJN-Cfr9Kji9.js.map → vennDiagram-CIIHVFJN-1jmBZPSZ.js.map} +1 -1
  93. package/dist/static/assets/{wardleyDiagram-YWT4CUSO-BU30t5hg.js → wardleyDiagram-YWT4CUSO-A8XYzYTW.js} +2 -2
  94. package/dist/static/assets/{wardleyDiagram-YWT4CUSO-BU30t5hg.js.map → wardleyDiagram-YWT4CUSO-A8XYzYTW.js.map} +1 -1
  95. package/dist/static/assets/{xychartDiagram-2RQKCTM6-CbAQZMXv.js → xychartDiagram-2RQKCTM6-CuyUQvFt.js} +2 -2
  96. package/dist/static/assets/{xychartDiagram-2RQKCTM6-CbAQZMXv.js.map → xychartDiagram-2RQKCTM6-CuyUQvFt.js.map} +1 -1
  97. package/dist/static/index.html +16 -16
  98. package/package.json +14 -14
  99. package/dist/static/assets/channel-DusP3IMn.js +0 -2
  100. package/dist/static/assets/classDiagram-4FO5ZUOK-SuK_oLpa.js +0 -2
  101. package/dist/static/assets/classDiagram-v2-Q7XG4LA2-DMlc0uaN.js +0 -2
  102. package/dist/static/assets/index-BfPuPCFp.js +0 -228
  103. package/dist/static/assets/index-BfPuPCFp.js.map +0 -1
  104. package/dist/static/assets/index-CyEgx13R.css +0 -1
  105. package/dist/static/assets/mermaid-parser.core-CZyyUBUW.js +0 -5
  106. package/dist/static/assets/stateDiagram-v2-BHNVJYJU-Fm1RY0qD.js +0 -2
package/dist/index.d.ts CHANGED
@@ -41,18 +41,18 @@ declare function wrapWithSession(upstream: AuthResolver, opts?: {
41
41
  *
42
42
  * The UI package owns the *browser-facing* surface only:
43
43
  *
44
- * GET /ui/ serves the SPA shell (HTML)
45
- * GET /ui/assets/* serves the SPA's static bundle (JS/CSS/etc.)
46
- * GET /ui/login login form (HTML)
47
- * POST /ui/login validate API key, set session cookie, redirect
48
- * POST /ui/logout clear the session cookie
44
+ * GET <path>/ serves the SPA shell (HTML)
45
+ * GET <path>/assets/* serves the SPA's static bundle (JS/CSS/etc.)
46
+ * GET <path>/login login form (HTML)
47
+ * POST <path>/login validate API key, set session cookie, redirect
48
+ * POST <path>/logout clear the session cookie
49
49
  *
50
50
  * Every JSON+SSE endpoint the SPA talks to (`/runs`, `/runs/:id`,
51
51
  * `/runs/:id/tool-calls`, `/runs/:id/stream`, `/runs/:id/cancel`,
52
52
  * `/runs/:id/input`, `/agents`, `/usage`) is owned by `@render-harness/web`.
53
53
  * This package doesn't redefine any of them.
54
54
  *
55
- * The session cookie set by `/ui/login` is honoured by web's auth resolver
55
+ * The session cookie set by the login route is honoured by web's auth resolver
56
56
  * because `serveWeb({ ui: true })` wraps `auth()` with {@link wrapWithSession}.
57
57
  */
58
58
 
package/dist/index.js CHANGED
@@ -69,7 +69,7 @@ var DEFAULT_STATIC_DIR = resolve(HERE, "..", "dist", "static");
69
69
  var FALLBACK_STATIC_DIR = resolve(HERE, "static");
70
70
  var SPA_INDEX = "index.html";
71
71
  function mountUi(opts) {
72
- const path = (opts.path ?? "/ui").replace(/\/+$/, "");
72
+ const path = normalizeMountPath(opts.path ?? "/ui");
73
73
  const cookie = buildCookieConfig({
74
74
  ...opts.cookieName !== void 0 ? { cookieName: opts.cookieName } : {},
75
75
  ...opts.cookieSecret !== void 0 ? { secret: opts.cookieSecret } : {},
@@ -112,21 +112,27 @@ function mountUi(opts) {
112
112
  c.header("cache-control", "no-cache, must-revalidate");
113
113
  return c.html(html);
114
114
  };
115
- opts.app.get(path, serveSpaShell);
115
+ const serveAsset = async (c, rel) => {
116
+ const buf = await readBundleAsset(staticDir, join("assets", rel));
117
+ if (!buf) return c.notFound();
118
+ const bytes = new Uint8Array(new ArrayBuffer(buf.byteLength));
119
+ bytes.set(buf);
120
+ return c.body(bytes, 200, {
121
+ "content-type": guessContentType(rel),
122
+ // Vite emits hash-named asset files; safe to cache aggressively.
123
+ "cache-control": "public, max-age=31536000, immutable"
124
+ });
125
+ };
126
+ if (path === "") {
127
+ opts.app.get("/assets/*", async (c) => serveAsset(c, c.req.path.replace(/^\/assets\//, "")));
128
+ }
129
+ opts.app.get(path || "/", serveSpaShell);
116
130
  opts.app.get(`${path}/`, serveSpaShell);
117
131
  opts.app.get(`${path}/*`, async (c, next) => {
118
132
  const sub = c.req.path.slice(path.length);
119
133
  if (sub.startsWith("/assets/")) {
120
134
  const rel = sub.replace(/^\/assets\//, "");
121
- const buf = await readBundleAsset(staticDir, join("assets", rel));
122
- if (!buf) return c.notFound();
123
- const bytes = new Uint8Array(new ArrayBuffer(buf.byteLength));
124
- bytes.set(buf);
125
- return c.body(bytes, 200, {
126
- "content-type": guessContentType(rel),
127
- // Vite emits hash-named asset files; safe to cache aggressively.
128
- "cache-control": "public, max-age=31536000, immutable"
129
- });
135
+ return serveAsset(c, rel);
130
136
  }
131
137
  if (sub === "/login" || sub.startsWith("/login?")) {
132
138
  return next();
@@ -134,6 +140,11 @@ function mountUi(opts) {
134
140
  return serveSpaShell(c);
135
141
  });
136
142
  }
143
+ function normalizeMountPath(raw) {
144
+ const withSlash = raw.startsWith("/") ? raw : `/${raw}`;
145
+ const trimmed = withSlash.replace(/\/+$/, "");
146
+ return trimmed === "" ? "" : trimmed;
147
+ }
137
148
  async function readBundleHtml(staticDir, relPath) {
138
149
  const buf = await readBundleBytes(staticDir, relPath);
139
150
  return buf ? buf.toString("utf8") : null;
@@ -179,12 +190,12 @@ var INLINE_THEME_CSS = `
179
190
  :root {
180
191
  color-scheme: light dark;
181
192
  --bg: #fff; --fg: #000; --muted: #555; --line: #000;
182
- --accent: #d97706; --err: #b40000;
193
+ --accent: #a855f7; --err: #b40000;
183
194
  }
184
195
  @media (prefers-color-scheme: dark) {
185
196
  :root {
186
- --bg: #000; --fg: #fff; --muted: #999; --line: #fff;
187
- --accent: #fbbf24; --err: #ff5555;
197
+ --bg: #000; --fg: #fff; --muted: #8a8a8a; --line: #2a2a2a;
198
+ --accent: #c084fc; --err: #ff5555;
188
199
  }
189
200
  }
190
201
  * { box-sizing: border-box; border-radius: 0 !important; }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/auth.ts","../src/server.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Cookie-session helpers for the operator UI.\n *\n * `@render-harness/web` accepts a `Bearer <api-key>` header and resolves\n * it to a `UserId`. A browser UI needs the same identity but without\n * making the user paste the bearer on every request, so we layer a signed\n * cookie on top:\n *\n * 1. Browser visits `<ui>/login`, posts the API key.\n * 2. We call the existing auth resolver with a synthetic\n * `Authorization: Bearer ...` request to validate the key.\n * 3. On success we set a signed cookie carrying the resolved `userId`.\n * 4. {@link wrapWithSession} is plugged into `serveWeb` so all routes\n * accept either the cookie OR the original bearer header. curl-based\n * ops still work; browsers stay logged in.\n *\n * The signing secret comes from `UI_COOKIE_SECRET` (env). If unset we\n * generate a random per-process secret and warn — fine for local dev,\n * means cookies don't survive a restart.\n */\n\nimport type { UserId } from \"@render-harness/core\";\nimport type { Context } from \"hono\";\nimport { deleteCookie, getSignedCookie, setSignedCookie } from \"hono/cookie\";\n\nexport type AuthResolver = (req: Request) => Promise<UserId | null>;\n\nexport interface CookieSessionConfig {\n cookieName: string;\n secret: string;\n /** Lifetime in seconds. Default: 7 days. */\n maxAge: number;\n /** Set Secure flag on the cookie. Default: true outside dev. */\n secure: boolean;\n}\n\nconst DEFAULT_COOKIE_NAME = \"rh_ui_session\";\nconst DEFAULT_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;\n\nexport function buildCookieConfig(opts: {\n cookieName?: string;\n secret?: string;\n maxAge?: number;\n secure?: boolean;\n}): CookieSessionConfig {\n const envSecret = process.env.UI_COOKIE_SECRET;\n const secret = opts.secret ?? envSecret ?? generateEphemeralSecret();\n const isDev = process.env.NODE_ENV !== \"production\";\n return {\n cookieName: opts.cookieName ?? DEFAULT_COOKIE_NAME,\n secret,\n maxAge: opts.maxAge ?? DEFAULT_MAX_AGE_SECONDS,\n secure: opts.secure ?? !isDev,\n };\n}\n\n/**\n * Read the signed session cookie from a `Request`, returning the userId\n * encoded in it or `null` if the cookie is missing/invalid.\n *\n * Hono's cookie helpers want a {@link Context}, but they only ever read\n * `c.req.raw.headers.get(\"Cookie\")` under the hood — so we pass a minimal\n * shim. Keeps verification in Hono's well-tested code without taking on\n * an internal dependency.\n */\nexport async function readSessionCookie(\n req: Request,\n cfg: CookieSessionConfig,\n): Promise<UserId | null> {\n const shim = { req: { raw: req } } as unknown as Context;\n const value = await getSignedCookie(shim, cfg.secret, cfg.cookieName);\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\n/**\n * Resolve the request to a `UserId`, accepting either the cookie session\n * or a bearer header. Returns `null` if neither validates. Used inside\n * `/ui/*` handlers where we already have the Hono Context.\n */\nexport async function resolveSession(\n c: Context,\n cfg: CookieSessionConfig,\n upstreamAuth: AuthResolver,\n): Promise<UserId | null> {\n const fromCookie = await readSessionCookie(c.req.raw, cfg);\n if (fromCookie) return fromCookie;\n return upstreamAuth(c.req.raw);\n}\n\n/**\n * Wrap an existing `(Request) => Promise<UserId | null>` resolver so it\n * also accepts the signed UI session cookie. The web service plugs the\n * wrapped resolver into all of its routes when `serveWeb({ ui: true })`,\n * so JSON+SSE callers can use either bearer or cookie.\n */\nexport function wrapWithSession(\n upstream: AuthResolver,\n opts: {\n cookieSecret?: string;\n cookieName?: string;\n cookieMaxAge?: number;\n cookieSecure?: boolean;\n } = {},\n): AuthResolver {\n const cfg = buildCookieConfig({\n ...(opts.cookieName !== undefined ? { cookieName: opts.cookieName } : {}),\n ...(opts.cookieSecret !== undefined ? { secret: opts.cookieSecret } : {}),\n ...(opts.cookieMaxAge !== undefined ? { maxAge: opts.cookieMaxAge } : {}),\n ...(opts.cookieSecure !== undefined ? { secure: opts.cookieSecure } : {}),\n });\n return async (req: Request) => {\n const fromCookie = await readSessionCookie(req, cfg);\n if (fromCookie) return fromCookie;\n return upstream(req);\n };\n}\n\n/**\n * Mint a signed session cookie for the given `userId`. Caller is expected\n * to have already validated the user.\n */\nexport async function issueSession(\n c: Context,\n cfg: CookieSessionConfig,\n userId: UserId,\n): Promise<void> {\n await setSignedCookie(c, cfg.cookieName, userId, cfg.secret, {\n path: \"/\",\n httpOnly: true,\n sameSite: \"Lax\",\n secure: cfg.secure,\n maxAge: cfg.maxAge,\n });\n}\n\nexport function clearSession(c: Context, cfg: CookieSessionConfig): void {\n deleteCookie(c, cfg.cookieName, { path: \"/\" });\n}\n\n/**\n * Forge a `Request` carrying `Authorization: Bearer <apiKey>` so we can\n * reuse the upstream resolver (which only knows how to read headers) for\n * the login form path.\n */\nexport function authRequestForBearer(apiKey: string): Request {\n return new Request(\"https://internal.invalid/login\", {\n headers: { authorization: `Bearer ${apiKey}` },\n });\n}\n\nfunction generateEphemeralSecret(): string {\n const bytes = new Uint8Array(32);\n globalThis.crypto.getRandomValues(bytes);\n let hex = \"\";\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i] ?? 0;\n hex += b.toString(16).padStart(2, \"0\");\n }\n return hex;\n}\n","/**\n * `mountUi(opts)` — attach the operator UI to an existing Hono app.\n *\n * The UI package owns the *browser-facing* surface only:\n *\n * GET /ui/ serves the SPA shell (HTML)\n * GET /ui/assets/* serves the SPA's static bundle (JS/CSS/etc.)\n * GET /ui/login login form (HTML)\n * POST /ui/login validate API key, set session cookie, redirect\n * POST /ui/logout clear the session cookie\n *\n * Every JSON+SSE endpoint the SPA talks to (`/runs`, `/runs/:id`,\n * `/runs/:id/tool-calls`, `/runs/:id/stream`, `/runs/:id/cancel`,\n * `/runs/:id/input`, `/agents`, `/usage`) is owned by `@render-harness/web`.\n * This package doesn't redefine any of them.\n *\n * The session cookie set by `/ui/login` is honoured by web's auth resolver\n * because `serveWeb({ ui: true })` wraps `auth()` with {@link wrapWithSession}.\n */\n\nimport { readFile, stat } from \"node:fs/promises\";\nimport { dirname, join, normalize, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Context, Hono } from \"hono\";\nimport {\n type AuthResolver,\n authRequestForBearer,\n buildCookieConfig,\n clearSession,\n issueSession,\n readSessionCookie,\n} from \"./auth.js\";\n\nexport type { AuthResolver } from \"./auth.js\";\nexport { wrapWithSession } from \"./auth.js\";\n\nexport interface MountUiOpts {\n /** Hono app the harness web service already built. */\n app: Hono;\n /**\n * Auth resolver from the parent. Used as the upstream check for the\n * login form; web's main `auth` already accepts the session cookie via\n * `wrapWithSession`, so we don't re-implement that here.\n */\n auth: AuthResolver;\n /** Mount path. Defaults to \"/ui\". */\n path?: string;\n /**\n * Cookie session secret. Falls back to `UI_COOKIE_SECRET` env, then to a\n * per-process random value (won't survive a restart — fine for dev).\n */\n cookieSecret?: string;\n /** Cookie name. Defaults to \"rh_ui_session\". */\n cookieName?: string;\n /** Cookie max age in seconds. Defaults to 7 days. */\n cookieMaxAge?: number;\n /** Force the Secure cookie flag. Defaults: true in production, false otherwise. */\n cookieSecure?: boolean;\n /**\n * Override the directory we serve the SPA bundle from. Defaults to the\n * `dist/static` shipped alongside this package's compiled JS.\n */\n staticDir?: string;\n}\n\nconst HERE = dirname(fileURLToPath(import.meta.url));\n// dist/index.js → dist/static. In dev (src/server.ts → src/../dist/static)\n// we still resolve up one level; we ship a built copy with the package.\nconst DEFAULT_STATIC_DIR = resolve(HERE, \"..\", \"dist\", \"static\");\nconst FALLBACK_STATIC_DIR = resolve(HERE, \"static\");\n\nconst SPA_INDEX = \"index.html\";\n\nexport function mountUi(opts: MountUiOpts): void {\n const path = (opts.path ?? \"/ui\").replace(/\\/+$/, \"\");\n const cookie = buildCookieConfig({\n ...(opts.cookieName !== undefined ? { cookieName: opts.cookieName } : {}),\n ...(opts.cookieSecret !== undefined ? { secret: opts.cookieSecret } : {}),\n ...(opts.cookieMaxAge !== undefined ? { maxAge: opts.cookieMaxAge } : {}),\n ...(opts.cookieSecure !== undefined ? { secure: opts.cookieSecure } : {}),\n });\n\n const staticDir = opts.staticDir ?? DEFAULT_STATIC_DIR;\n\n // Login / logout\n opts.app.get(`${path}/login`, (c) =>\n c.html(renderLoginPage({ uiPath: path, error: c.req.query(\"error\") ?? null })),\n );\n\n opts.app.post(`${path}/login`, async (c) => {\n const form = await c.req.formData().catch(() => null);\n const apiKey = typeof form?.get(\"apiKey\") === \"string\" ? (form?.get(\"apiKey\") as string) : \"\";\n if (!apiKey) {\n return c.redirect(`${path}/login?error=missing`, 303);\n }\n const userId = await opts.auth(authRequestForBearer(apiKey));\n if (!userId) {\n return c.redirect(`${path}/login?error=invalid`, 303);\n }\n await issueSession(c, cookie, userId);\n const next = c.req.query(\"next\") ?? `${path}/`;\n return c.redirect(safeNextPath(next, path), 303);\n });\n\n opts.app.post(`${path}/logout`, (c) => {\n clearSession(c, cookie);\n return c.redirect(`${path}/login`, 303);\n });\n\n // SPA shell + assets. The shell itself is gated by the cookie session;\n // assets are not (they're cacheable static files).\n const serveSpaShell = async (c: Context) => {\n const userId = await readSessionCookie(c.req.raw, cookie);\n if (!userId) {\n const next = encodeURIComponent(c.req.path);\n return c.redirect(`${path}/login?next=${next}`, 303);\n }\n const html = await readBundleHtml(staticDir, SPA_INDEX);\n if (html === null) {\n return c.html(renderBuildMissingPage({ uiPath: path }), 503);\n }\n // The SPA shell is tiny and references hash-named asset bundles. We\n // don't want stale cached HTML pointing at deleted asset hashes, so\n // browsers must revalidate every load. Asset files (under\n // /ui/assets/*) are content-addressed and stay cacheable.\n c.header(\"cache-control\", \"no-cache, must-revalidate\");\n return c.html(html);\n };\n\n opts.app.get(path, serveSpaShell);\n opts.app.get(`${path}/`, serveSpaShell);\n opts.app.get(`${path}/*`, async (c, next) => {\n const sub = c.req.path.slice(path.length);\n if (sub.startsWith(\"/assets/\")) {\n const rel = sub.replace(/^\\/assets\\//, \"\");\n const buf = await readBundleAsset(staticDir, join(\"assets\", rel));\n if (!buf) return c.notFound();\n // Hono's typed body() wants `Uint8Array<ArrayBuffer>`, not Node's\n // `Buffer<ArrayBufferLike>`. Copy into a fresh ArrayBuffer-backed\n // view so the type matches without resorting to `any`.\n const bytes = new Uint8Array(new ArrayBuffer(buf.byteLength));\n bytes.set(buf);\n return c.body(bytes, 200, {\n \"content-type\": guessContentType(rel),\n // Vite emits hash-named asset files; safe to cache aggressively.\n \"cache-control\": \"public, max-age=31536000, immutable\",\n });\n }\n if (sub === \"/login\" || sub.startsWith(\"/login?\")) {\n return next();\n }\n return serveSpaShell(c);\n });\n}\n\n// --------------------------------------------------------------------\n// Static file helpers\n// --------------------------------------------------------------------\n\nasync function readBundleHtml(staticDir: string, relPath: string): Promise<string | null> {\n const buf = await readBundleBytes(staticDir, relPath);\n return buf ? buf.toString(\"utf8\") : null;\n}\n\nasync function readBundleAsset(staticDir: string, relPath: string): Promise<Buffer | null> {\n return readBundleBytes(staticDir, relPath);\n}\n\nasync function readBundleBytes(staticDir: string, relPath: string): Promise<Buffer | null> {\n const candidates = [staticDir, FALLBACK_STATIC_DIR];\n for (const dir of candidates) {\n const safe = safeJoin(dir, relPath);\n if (!safe) continue;\n try {\n const s = await stat(safe);\n if (!s.isFile()) continue;\n return await readFile(safe);\n } catch {\n // try the next candidate directory\n }\n }\n return null;\n}\n\nfunction safeJoin(root: string, rel: string): string | null {\n const resolved = normalize(join(root, rel));\n const rootResolved = `${normalize(root)}/`.replace(/\\/+$/, \"/\");\n return resolved.startsWith(rootResolved) || resolved === normalize(root) ? resolved : null;\n}\n\nfunction guessContentType(rel: string): string {\n if (rel.endsWith(\".js\") || rel.endsWith(\".mjs\")) return \"application/javascript; charset=utf-8\";\n if (rel.endsWith(\".css\")) return \"text/css; charset=utf-8\";\n if (rel.endsWith(\".svg\")) return \"image/svg+xml\";\n if (rel.endsWith(\".png\")) return \"image/png\";\n if (rel.endsWith(\".woff2\")) return \"font/woff2\";\n if (rel.endsWith(\".json\")) return \"application/json; charset=utf-8\";\n if (rel.endsWith(\".map\")) return \"application/json; charset=utf-8\";\n return \"application/octet-stream\";\n}\n\nfunction safeNextPath(next: string, uiPath: string): string {\n // Only allow same-origin redirects under the UI path prefix to avoid\n // open-redirects via the `next` query param.\n if (!next.startsWith(\"/\")) return `${uiPath}/`;\n if (next.startsWith(\"//\")) return `${uiPath}/`;\n return next;\n}\n\n// --------------------------------------------------------------------\n// Inline HTML helpers (login + the \"build missing\" fallback)\n// --------------------------------------------------------------------\n\n// Brutalist black-and-white CSS shared by the inline pages we render\n// outside the SPA bundle (login form, \"build missing\" fallback). Kept\n// here so server-only routes don't depend on the Tailwind output.\n//\n// Single accent: amber. Used for active states, focus, selection, and\n// the blinking input caret. Same palette as the SPA so the visual\n// transition into the dashboard is seamless.\nconst INLINE_THEME_CSS = `\n:root {\n color-scheme: light dark;\n --bg: #fff; --fg: #000; --muted: #555; --line: #000;\n --accent: #d97706; --err: #b40000;\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --bg: #000; --fg: #fff; --muted: #999; --line: #fff;\n --accent: #fbbf24; --err: #ff5555;\n }\n}\n* { box-sizing: border-box; border-radius: 0 !important; }\nhtml, body { height: 100%; }\nbody {\n margin: 0; background: var(--bg); color: var(--fg);\n font: 14px/1.45 ui-monospace, \"JetBrains Mono\", \"IBM Plex Mono\", \"SF Mono\", Menlo, Consolas, monospace;\n -webkit-font-smoothing: antialiased;\n}\n::selection { background: var(--accent); color: var(--bg); }\n.label {\n text-transform: uppercase; letter-spacing: 0.08em;\n font-size: 0.72rem; color: var(--muted);\n}\n.panel { border: 1px solid var(--line); background: var(--bg); }\n.hr-section {\n display: flex; align-items: center; gap: 0.6rem;\n text-transform: uppercase; letter-spacing: 0.1em;\n font-size: 0.72rem; color: var(--muted);\n margin: 0 0 1rem;\n}\n.hr-section::after {\n content: \"\"; flex: 1; border-top: 1px solid var(--line);\n}\n.btn {\n display: inline-flex; align-items: center; justify-content: center;\n width: 100%; padding: 0.6rem 0.7rem; border: 1px solid var(--accent);\n background: var(--accent); color: var(--bg); cursor: pointer;\n font: inherit; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.78rem;\n}\n.btn:hover { background: var(--bg); color: var(--accent); }\ninput {\n width: 100%; padding: 0.55rem 0.65rem; font: inherit; color: inherit;\n background: var(--bg); border: 1px solid var(--line);\n caret-color: var(--accent);\n}\ninput:focus-visible {\n outline: 1px solid var(--accent); outline-offset: 0;\n border-color: var(--accent);\n animation: rh-caret-blink 1.06s steps(2, jump-none) infinite;\n}\n@keyframes rh-caret-blink {\n 0%, 50% { caret-color: var(--accent); }\n 51%, 100% { caret-color: transparent; }\n}\n.blink { animation: rh-block-blink 1.06s steps(2, jump-none) infinite; }\n@keyframes rh-block-blink {\n 0%, 50% { opacity: 1; }\n 51%, 100% { opacity: 0; }\n}\n.cursor { color: var(--accent); }\ncode { font-family: inherit; }\n`;\n\nfunction renderLoginPage(args: { uiPath: string; error: string | null }): string {\n const errorHtml =\n args.error === \"invalid\"\n ? '<p class=\"err\">// invalid api key</p>'\n : args.error === \"missing\"\n ? '<p class=\"err\">// enter the api key configured as <code>WEB_API_KEY</code></p>'\n : \"\";\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>render-harness / operator / sign-in</title>\n <style>\n ${INLINE_THEME_CSS}\n main { min-height: 100vh; display: grid; place-items: center; padding: 2rem 1rem; }\n .card { width: min(440px, 100%); padding: 1.75rem; }\n .card h1 {\n margin: 0; font-size: 0.85rem; text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n .card h1 .muted { color: var(--muted); }\n .card .sub {\n margin: 0.25rem 0 1.5rem; color: var(--muted);\n font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em;\n }\n .card p.help { margin: 0 0 1.25rem; color: var(--muted); font-size: 0.78rem; }\n .field { margin-bottom: 0.9rem; }\n .field label { display: block; margin-bottom: 0.35rem; }\n .prompt {\n display: flex; align-items: stretch; gap: 0.5rem;\n }\n .prompt .gutter {\n display: flex; align-items: center;\n color: var(--accent); font-size: 0.85rem;\n }\n .err { margin: 0.6rem 0 0; color: var(--err); font-size: 0.78rem; }\n </style>\n </head>\n <body>\n <main>\n <form class=\"panel card\" method=\"post\" action=\"${escapeHtml(args.uiPath)}/login\">\n <h1>\n <span class=\"muted\">render-harness</span> / operator<span class=\"cursor blink\" aria-hidden=\"true\">▊</span>\n </h1>\n <p class=\"sub\">/sign-in</p>\n <p class=\"help\">// enter the api key configured for this service (<code>WEB_API_KEY</code>)</p>\n <div class=\"field\">\n <label class=\"label\" for=\"apiKey\">api key</label>\n <div class=\"prompt\">\n <span class=\"gutter\" aria-hidden=\"true\">$</span>\n <input id=\"apiKey\" name=\"apiKey\" type=\"password\" autocomplete=\"current-password\" required autofocus />\n </div>\n </div>\n <button class=\"btn\" type=\"submit\">[ continue ]</button>\n ${errorHtml}\n </form>\n </main>\n </body>\n</html>`;\n}\n\nfunction renderBuildMissingPage(args: { uiPath: string }): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>render-harness / operator / build-missing</title>\n <style>\n ${INLINE_THEME_CSS}\n main { max-width: 60ch; margin: 4rem auto; padding: 0 1.5rem; }\n h1 { font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.08em; margin: 0 0 1rem; }\n .box { padding: 1rem; }\n code { padding: 0.05rem 0.3rem; border: 1px solid var(--line); }\n </style>\n </head>\n <body>\n <main>\n <h1>// operator ui bundle missing</h1>\n <div class=\"panel box\">\n <p>The SPA bundle for <code>@render-harness/ui</code> hasn't been built. Run</p>\n <p><code>pnpm --filter @render-harness/ui build</code></p>\n <p>to compile it, then reload <code>${escapeHtml(args.uiPath)}/</code>.</p>\n </div>\n </main>\n </body>\n</html>`;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (ch) => {\n switch (ch) {\n case \"&\":\n return \"&amp;\";\n case \"<\":\n return \"&lt;\";\n case \">\":\n return \"&gt;\";\n case '\"':\n return \"&quot;\";\n default:\n return \"&#39;\";\n }\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/auth.ts","../src/server.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Cookie-session helpers for the operator UI.\n *\n * `@render-harness/web` accepts a `Bearer <api-key>` header and resolves\n * it to a `UserId`. A browser UI needs the same identity but without\n * making the user paste the bearer on every request, so we layer a signed\n * cookie on top:\n *\n * 1. Browser visits `<ui>/login`, posts the API key.\n * 2. We call the existing auth resolver with a synthetic\n * `Authorization: Bearer ...` request to validate the key.\n * 3. On success we set a signed cookie carrying the resolved `userId`.\n * 4. {@link wrapWithSession} is plugged into `serveWeb` so all routes\n * accept either the cookie OR the original bearer header. curl-based\n * ops still work; browsers stay logged in.\n *\n * The signing secret comes from `UI_COOKIE_SECRET` (env). If unset we\n * generate a random per-process secret and warn — fine for local dev,\n * means cookies don't survive a restart.\n */\n\nimport type { UserId } from \"@render-harness/core\";\nimport type { Context } from \"hono\";\nimport { deleteCookie, getSignedCookie, setSignedCookie } from \"hono/cookie\";\n\nexport type AuthResolver = (req: Request) => Promise<UserId | null>;\n\nexport interface CookieSessionConfig {\n cookieName: string;\n secret: string;\n /** Lifetime in seconds. Default: 7 days. */\n maxAge: number;\n /** Set Secure flag on the cookie. Default: true outside dev. */\n secure: boolean;\n}\n\nconst DEFAULT_COOKIE_NAME = \"rh_ui_session\";\nconst DEFAULT_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;\n\nexport function buildCookieConfig(opts: {\n cookieName?: string;\n secret?: string;\n maxAge?: number;\n secure?: boolean;\n}): CookieSessionConfig {\n const envSecret = process.env.UI_COOKIE_SECRET;\n const secret = opts.secret ?? envSecret ?? generateEphemeralSecret();\n const isDev = process.env.NODE_ENV !== \"production\";\n return {\n cookieName: opts.cookieName ?? DEFAULT_COOKIE_NAME,\n secret,\n maxAge: opts.maxAge ?? DEFAULT_MAX_AGE_SECONDS,\n secure: opts.secure ?? !isDev,\n };\n}\n\n/**\n * Read the signed session cookie from a `Request`, returning the userId\n * encoded in it or `null` if the cookie is missing/invalid.\n *\n * Hono's cookie helpers want a {@link Context}, but they only ever read\n * `c.req.raw.headers.get(\"Cookie\")` under the hood — so we pass a minimal\n * shim. Keeps verification in Hono's well-tested code without taking on\n * an internal dependency.\n */\nexport async function readSessionCookie(\n req: Request,\n cfg: CookieSessionConfig,\n): Promise<UserId | null> {\n const shim = { req: { raw: req } } as unknown as Context;\n const value = await getSignedCookie(shim, cfg.secret, cfg.cookieName);\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\n/**\n * Resolve the request to a `UserId`, accepting either the cookie session\n * or a bearer header. Returns `null` if neither validates. Used inside\n * `/ui/*` handlers where we already have the Hono Context.\n */\nexport async function resolveSession(\n c: Context,\n cfg: CookieSessionConfig,\n upstreamAuth: AuthResolver,\n): Promise<UserId | null> {\n const fromCookie = await readSessionCookie(c.req.raw, cfg);\n if (fromCookie) return fromCookie;\n return upstreamAuth(c.req.raw);\n}\n\n/**\n * Wrap an existing `(Request) => Promise<UserId | null>` resolver so it\n * also accepts the signed UI session cookie. The web service plugs the\n * wrapped resolver into all of its routes when `serveWeb({ ui: true })`,\n * so JSON+SSE callers can use either bearer or cookie.\n */\nexport function wrapWithSession(\n upstream: AuthResolver,\n opts: {\n cookieSecret?: string;\n cookieName?: string;\n cookieMaxAge?: number;\n cookieSecure?: boolean;\n } = {},\n): AuthResolver {\n const cfg = buildCookieConfig({\n ...(opts.cookieName !== undefined ? { cookieName: opts.cookieName } : {}),\n ...(opts.cookieSecret !== undefined ? { secret: opts.cookieSecret } : {}),\n ...(opts.cookieMaxAge !== undefined ? { maxAge: opts.cookieMaxAge } : {}),\n ...(opts.cookieSecure !== undefined ? { secure: opts.cookieSecure } : {}),\n });\n return async (req: Request) => {\n const fromCookie = await readSessionCookie(req, cfg);\n if (fromCookie) return fromCookie;\n return upstream(req);\n };\n}\n\n/**\n * Mint a signed session cookie for the given `userId`. Caller is expected\n * to have already validated the user.\n */\nexport async function issueSession(\n c: Context,\n cfg: CookieSessionConfig,\n userId: UserId,\n): Promise<void> {\n await setSignedCookie(c, cfg.cookieName, userId, cfg.secret, {\n path: \"/\",\n httpOnly: true,\n sameSite: \"Lax\",\n secure: cfg.secure,\n maxAge: cfg.maxAge,\n });\n}\n\nexport function clearSession(c: Context, cfg: CookieSessionConfig): void {\n deleteCookie(c, cfg.cookieName, { path: \"/\" });\n}\n\n/**\n * Forge a `Request` carrying `Authorization: Bearer <apiKey>` so we can\n * reuse the upstream resolver (which only knows how to read headers) for\n * the login form path.\n */\nexport function authRequestForBearer(apiKey: string): Request {\n return new Request(\"https://internal.invalid/login\", {\n headers: { authorization: `Bearer ${apiKey}` },\n });\n}\n\nfunction generateEphemeralSecret(): string {\n const bytes = new Uint8Array(32);\n globalThis.crypto.getRandomValues(bytes);\n let hex = \"\";\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i] ?? 0;\n hex += b.toString(16).padStart(2, \"0\");\n }\n return hex;\n}\n","/**\n * `mountUi(opts)` — attach the operator UI to an existing Hono app.\n *\n * The UI package owns the *browser-facing* surface only:\n *\n * GET <path>/ serves the SPA shell (HTML)\n * GET <path>/assets/* serves the SPA's static bundle (JS/CSS/etc.)\n * GET <path>/login login form (HTML)\n * POST <path>/login validate API key, set session cookie, redirect\n * POST <path>/logout clear the session cookie\n *\n * Every JSON+SSE endpoint the SPA talks to (`/runs`, `/runs/:id`,\n * `/runs/:id/tool-calls`, `/runs/:id/stream`, `/runs/:id/cancel`,\n * `/runs/:id/input`, `/agents`, `/usage`) is owned by `@render-harness/web`.\n * This package doesn't redefine any of them.\n *\n * The session cookie set by the login route is honoured by web's auth resolver\n * because `serveWeb({ ui: true })` wraps `auth()` with {@link wrapWithSession}.\n */\n\nimport { readFile, stat } from \"node:fs/promises\";\nimport { dirname, join, normalize, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { Context, Hono } from \"hono\";\nimport {\n type AuthResolver,\n authRequestForBearer,\n buildCookieConfig,\n clearSession,\n issueSession,\n readSessionCookie,\n} from \"./auth.js\";\n\nexport type { AuthResolver } from \"./auth.js\";\nexport { wrapWithSession } from \"./auth.js\";\n\nexport interface MountUiOpts {\n /** Hono app the harness web service already built. */\n app: Hono;\n /**\n * Auth resolver from the parent. Used as the upstream check for the\n * login form; web's main `auth` already accepts the session cookie via\n * `wrapWithSession`, so we don't re-implement that here.\n */\n auth: AuthResolver;\n /** Mount path. Defaults to \"/ui\". */\n path?: string;\n /**\n * Cookie session secret. Falls back to `UI_COOKIE_SECRET` env, then to a\n * per-process random value (won't survive a restart — fine for dev).\n */\n cookieSecret?: string;\n /** Cookie name. Defaults to \"rh_ui_session\". */\n cookieName?: string;\n /** Cookie max age in seconds. Defaults to 7 days. */\n cookieMaxAge?: number;\n /** Force the Secure cookie flag. Defaults: true in production, false otherwise. */\n cookieSecure?: boolean;\n /**\n * Override the directory we serve the SPA bundle from. Defaults to the\n * `dist/static` shipped alongside this package's compiled JS.\n */\n staticDir?: string;\n}\n\nconst HERE = dirname(fileURLToPath(import.meta.url));\n// dist/index.js → dist/static. In dev (src/server.ts → src/../dist/static)\n// we still resolve up one level; we ship a built copy with the package.\nconst DEFAULT_STATIC_DIR = resolve(HERE, \"..\", \"dist\", \"static\");\nconst FALLBACK_STATIC_DIR = resolve(HERE, \"static\");\n\nconst SPA_INDEX = \"index.html\";\n\nexport function mountUi(opts: MountUiOpts): void {\n const path = normalizeMountPath(opts.path ?? \"/ui\");\n const cookie = buildCookieConfig({\n ...(opts.cookieName !== undefined ? { cookieName: opts.cookieName } : {}),\n ...(opts.cookieSecret !== undefined ? { secret: opts.cookieSecret } : {}),\n ...(opts.cookieMaxAge !== undefined ? { maxAge: opts.cookieMaxAge } : {}),\n ...(opts.cookieSecure !== undefined ? { secure: opts.cookieSecure } : {}),\n });\n\n const staticDir = opts.staticDir ?? DEFAULT_STATIC_DIR;\n\n // Login / logout\n opts.app.get(`${path}/login`, (c) =>\n c.html(renderLoginPage({ uiPath: path, error: c.req.query(\"error\") ?? null })),\n );\n\n opts.app.post(`${path}/login`, async (c) => {\n const form = await c.req.formData().catch(() => null);\n const apiKey = typeof form?.get(\"apiKey\") === \"string\" ? (form?.get(\"apiKey\") as string) : \"\";\n if (!apiKey) {\n return c.redirect(`${path}/login?error=missing`, 303);\n }\n const userId = await opts.auth(authRequestForBearer(apiKey));\n if (!userId) {\n return c.redirect(`${path}/login?error=invalid`, 303);\n }\n await issueSession(c, cookie, userId);\n const next = c.req.query(\"next\") ?? `${path}/`;\n return c.redirect(safeNextPath(next, path), 303);\n });\n\n opts.app.post(`${path}/logout`, (c) => {\n clearSession(c, cookie);\n return c.redirect(`${path}/login`, 303);\n });\n\n // SPA shell + assets. The shell itself is gated by the cookie session;\n // assets are not (they're cacheable static files).\n const serveSpaShell = async (c: Context) => {\n const userId = await readSessionCookie(c.req.raw, cookie);\n if (!userId) {\n const next = encodeURIComponent(c.req.path);\n return c.redirect(`${path}/login?next=${next}`, 303);\n }\n const html = await readBundleHtml(staticDir, SPA_INDEX);\n if (html === null) {\n return c.html(renderBuildMissingPage({ uiPath: path }), 503);\n }\n // The SPA shell is tiny and references hash-named asset bundles. We\n // don't want stale cached HTML pointing at deleted asset hashes, so\n // browsers must revalidate every load. Asset files (under\n // <path>/assets/*) are content-addressed and stay cacheable.\n c.header(\"cache-control\", \"no-cache, must-revalidate\");\n return c.html(html);\n };\n\n const serveAsset = async (c: Context, rel: string) => {\n const buf = await readBundleAsset(staticDir, join(\"assets\", rel));\n if (!buf) return c.notFound();\n // Hono's typed body() wants `Uint8Array<ArrayBuffer>`, not Node's\n // `Buffer<ArrayBufferLike>`. Copy into a fresh ArrayBuffer-backed\n // view so the type matches without resorting to `any`.\n const bytes = new Uint8Array(new ArrayBuffer(buf.byteLength));\n bytes.set(buf);\n return c.body(bytes, 200, {\n \"content-type\": guessContentType(rel),\n // Vite emits hash-named asset files; safe to cache aggressively.\n \"cache-control\": \"public, max-age=31536000, immutable\",\n });\n };\n\n if (path === \"\") {\n opts.app.get(\"/assets/*\", async (c) => serveAsset(c, c.req.path.replace(/^\\/assets\\//, \"\")));\n }\n\n opts.app.get(path || \"/\", serveSpaShell);\n opts.app.get(`${path}/`, serveSpaShell);\n opts.app.get(`${path}/*`, async (c, next) => {\n const sub = c.req.path.slice(path.length);\n if (sub.startsWith(\"/assets/\")) {\n const rel = sub.replace(/^\\/assets\\//, \"\");\n return serveAsset(c, rel);\n }\n if (sub === \"/login\" || sub.startsWith(\"/login?\")) {\n return next();\n }\n return serveSpaShell(c);\n });\n}\n\nfunction normalizeMountPath(raw: string): string {\n const withSlash = raw.startsWith(\"/\") ? raw : `/${raw}`;\n const trimmed = withSlash.replace(/\\/+$/, \"\");\n return trimmed === \"\" ? \"\" : trimmed;\n}\n\n// --------------------------------------------------------------------\n// Static file helpers\n// --------------------------------------------------------------------\n\nasync function readBundleHtml(staticDir: string, relPath: string): Promise<string | null> {\n const buf = await readBundleBytes(staticDir, relPath);\n return buf ? buf.toString(\"utf8\") : null;\n}\n\nasync function readBundleAsset(staticDir: string, relPath: string): Promise<Buffer | null> {\n return readBundleBytes(staticDir, relPath);\n}\n\nasync function readBundleBytes(staticDir: string, relPath: string): Promise<Buffer | null> {\n const candidates = [staticDir, FALLBACK_STATIC_DIR];\n for (const dir of candidates) {\n const safe = safeJoin(dir, relPath);\n if (!safe) continue;\n try {\n const s = await stat(safe);\n if (!s.isFile()) continue;\n return await readFile(safe);\n } catch {\n // try the next candidate directory\n }\n }\n return null;\n}\n\nfunction safeJoin(root: string, rel: string): string | null {\n const resolved = normalize(join(root, rel));\n const rootResolved = `${normalize(root)}/`.replace(/\\/+$/, \"/\");\n return resolved.startsWith(rootResolved) || resolved === normalize(root) ? resolved : null;\n}\n\nfunction guessContentType(rel: string): string {\n if (rel.endsWith(\".js\") || rel.endsWith(\".mjs\")) return \"application/javascript; charset=utf-8\";\n if (rel.endsWith(\".css\")) return \"text/css; charset=utf-8\";\n if (rel.endsWith(\".svg\")) return \"image/svg+xml\";\n if (rel.endsWith(\".png\")) return \"image/png\";\n if (rel.endsWith(\".woff2\")) return \"font/woff2\";\n if (rel.endsWith(\".json\")) return \"application/json; charset=utf-8\";\n if (rel.endsWith(\".map\")) return \"application/json; charset=utf-8\";\n return \"application/octet-stream\";\n}\n\nfunction safeNextPath(next: string, uiPath: string): string {\n // Only allow same-origin redirects under the UI path prefix to avoid\n // open-redirects via the `next` query param.\n if (!next.startsWith(\"/\")) return `${uiPath}/`;\n if (next.startsWith(\"//\")) return `${uiPath}/`;\n return next;\n}\n\n// --------------------------------------------------------------------\n// Inline HTML helpers (login + the \"build missing\" fallback)\n// --------------------------------------------------------------------\n\n// Brutalist black-and-white CSS shared by the inline pages we render\n// outside the SPA bundle (login form, \"build missing\" fallback). Kept\n// here so server-only routes don't depend on the Tailwind output.\n//\n// Single accent: purple. Used for active states, focus, selection, and\n// the blinking input caret. Same palette as the SPA so the visual\n// transition into the dashboard is seamless.\nconst INLINE_THEME_CSS = `\n:root {\n color-scheme: light dark;\n --bg: #fff; --fg: #000; --muted: #555; --line: #000;\n --accent: #a855f7; --err: #b40000;\n}\n@media (prefers-color-scheme: dark) {\n :root {\n --bg: #000; --fg: #fff; --muted: #8a8a8a; --line: #2a2a2a;\n --accent: #c084fc; --err: #ff5555;\n }\n}\n* { box-sizing: border-box; border-radius: 0 !important; }\nhtml, body { height: 100%; }\nbody {\n margin: 0; background: var(--bg); color: var(--fg);\n font: 14px/1.45 ui-monospace, \"JetBrains Mono\", \"IBM Plex Mono\", \"SF Mono\", Menlo, Consolas, monospace;\n -webkit-font-smoothing: antialiased;\n}\n::selection { background: var(--accent); color: var(--bg); }\n.label {\n text-transform: uppercase; letter-spacing: 0.08em;\n font-size: 0.72rem; color: var(--muted);\n}\n.panel { border: 1px solid var(--line); background: var(--bg); }\n.hr-section {\n display: flex; align-items: center; gap: 0.6rem;\n text-transform: uppercase; letter-spacing: 0.1em;\n font-size: 0.72rem; color: var(--muted);\n margin: 0 0 1rem;\n}\n.hr-section::after {\n content: \"\"; flex: 1; border-top: 1px solid var(--line);\n}\n.btn {\n display: inline-flex; align-items: center; justify-content: center;\n width: 100%; padding: 0.6rem 0.7rem; border: 1px solid var(--accent);\n background: var(--accent); color: var(--bg); cursor: pointer;\n font: inherit; text-transform: uppercase; letter-spacing: 0.06em; font-size: 0.78rem;\n}\n.btn:hover { background: var(--bg); color: var(--accent); }\ninput {\n width: 100%; padding: 0.55rem 0.65rem; font: inherit; color: inherit;\n background: var(--bg); border: 1px solid var(--line);\n caret-color: var(--accent);\n}\ninput:focus-visible {\n outline: 1px solid var(--accent); outline-offset: 0;\n border-color: var(--accent);\n animation: rh-caret-blink 1.06s steps(2, jump-none) infinite;\n}\n@keyframes rh-caret-blink {\n 0%, 50% { caret-color: var(--accent); }\n 51%, 100% { caret-color: transparent; }\n}\n.blink { animation: rh-block-blink 1.06s steps(2, jump-none) infinite; }\n@keyframes rh-block-blink {\n 0%, 50% { opacity: 1; }\n 51%, 100% { opacity: 0; }\n}\n.cursor { color: var(--accent); }\ncode { font-family: inherit; }\n`;\n\nfunction renderLoginPage(args: { uiPath: string; error: string | null }): string {\n const errorHtml =\n args.error === \"invalid\"\n ? '<p class=\"err\">// invalid api key</p>'\n : args.error === \"missing\"\n ? '<p class=\"err\">// enter the api key configured as <code>WEB_API_KEY</code></p>'\n : \"\";\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>render-harness / operator / sign-in</title>\n <style>\n ${INLINE_THEME_CSS}\n main { min-height: 100vh; display: grid; place-items: center; padding: 2rem 1rem; }\n .card { width: min(440px, 100%); padding: 1.75rem; }\n .card h1 {\n margin: 0; font-size: 0.85rem; text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n .card h1 .muted { color: var(--muted); }\n .card .sub {\n margin: 0.25rem 0 1.5rem; color: var(--muted);\n font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em;\n }\n .card p.help { margin: 0 0 1.25rem; color: var(--muted); font-size: 0.78rem; }\n .field { margin-bottom: 0.9rem; }\n .field label { display: block; margin-bottom: 0.35rem; }\n .prompt {\n display: flex; align-items: stretch; gap: 0.5rem;\n }\n .prompt .gutter {\n display: flex; align-items: center;\n color: var(--accent); font-size: 0.85rem;\n }\n .err { margin: 0.6rem 0 0; color: var(--err); font-size: 0.78rem; }\n </style>\n </head>\n <body>\n <main>\n <form class=\"panel card\" method=\"post\" action=\"${escapeHtml(args.uiPath)}/login\">\n <h1>\n <span class=\"muted\">render-harness</span> / operator<span class=\"cursor blink\" aria-hidden=\"true\">▊</span>\n </h1>\n <p class=\"sub\">/sign-in</p>\n <p class=\"help\">// enter the api key configured for this service (<code>WEB_API_KEY</code>)</p>\n <div class=\"field\">\n <label class=\"label\" for=\"apiKey\">api key</label>\n <div class=\"prompt\">\n <span class=\"gutter\" aria-hidden=\"true\">$</span>\n <input id=\"apiKey\" name=\"apiKey\" type=\"password\" autocomplete=\"current-password\" required autofocus />\n </div>\n </div>\n <button class=\"btn\" type=\"submit\">[ continue ]</button>\n ${errorHtml}\n </form>\n </main>\n </body>\n</html>`;\n}\n\nfunction renderBuildMissingPage(args: { uiPath: string }): string {\n return `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>render-harness / operator / build-missing</title>\n <style>\n ${INLINE_THEME_CSS}\n main { max-width: 60ch; margin: 4rem auto; padding: 0 1.5rem; }\n h1 { font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.08em; margin: 0 0 1rem; }\n .box { padding: 1rem; }\n code { padding: 0.05rem 0.3rem; border: 1px solid var(--line); }\n </style>\n </head>\n <body>\n <main>\n <h1>// operator ui bundle missing</h1>\n <div class=\"panel box\">\n <p>The SPA bundle for <code>@render-harness/ui</code> hasn't been built. Run</p>\n <p><code>pnpm --filter @render-harness/ui build</code></p>\n <p>to compile it, then reload <code>${escapeHtml(args.uiPath)}/</code>.</p>\n </div>\n </main>\n </body>\n</html>`;\n}\n\nfunction escapeHtml(s: string): string {\n return s.replace(/[&<>\"']/g, (ch) => {\n switch (ch) {\n case \"&\":\n return \"&amp;\";\n case \"<\":\n return \"&lt;\";\n case \">\":\n return \"&gt;\";\n case '\"':\n return \"&quot;\";\n default:\n return \"&#39;\";\n }\n });\n}\n"]}