repoburg 1.3.217 → 1.3.219
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/agent-pane/frontend/dist/assets/index-BUkOj2Cb.js +392 -0
- package/agent-pane/frontend/dist/assets/index-gAFaOmjN.css +1 -0
- package/agent-pane/frontend/dist/index.html +19 -0
- package/daemon/dist/agentPane.js +13 -8
- package/daemon/dist/auth/authManager.js +9 -0
- package/daemon/dist/index.js +32 -5
- package/daemon/dist/middleware/checkRemoteAuth.js +72 -0
- package/daemon/dist/middleware/checkRemoteAuth.spec.js +117 -0
- package/daemon/dist/middleware/corsOrigin.js +53 -0
- package/daemon/dist/middleware/corsOrigin.spec.js +46 -0
- package/daemon/dist/remote/access-jwt.js +67 -0
- package/daemon/dist/remote/access-jwt.spec.js +94 -0
- package/daemon/dist/remote/cloudflared.js +239 -0
- package/daemon/dist/remote/cloudflared.spec.js +117 -0
- package/daemon/dist/remote/orchestrator.js +245 -0
- package/daemon/dist/remote/orchestrator.spec.js +192 -0
- package/daemon/dist/remote/probes.js +59 -0
- package/daemon/dist/remote/provisioner.js +86 -0
- package/daemon/dist/remote/provisioner.spec.js +104 -0
- package/daemon/dist/remote/routes.js +155 -0
- package/daemon/dist/remote/routes.spec.js +245 -0
- package/daemon/dist/remote/tunnel-state.js +54 -0
- package/daemon/dist/remote/tunnel-state.spec.js +40 -0
- package/daemon/dist/staticServing.js +44 -0
- package/daemon/dist/staticServing.spec.js +85 -0
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import"https://fonts.googleapis.com/css2?family=Inter:wght@400;450;510;590;600&family=JetBrains+Mono:wght@400;500&display=swap";:root{--canvas: #fdfdfc;--surface-0: #f8f7f5;--surface-1: #f0efed;--surface-2: #e6e4e0;--surface-3: #d6d3cd;--border: #e2e0db;--border-muted: #edebe7;--border-hover: #ccc8c0;--text: #1c1b18;--text-2: #54504a;--text-3: #78736c;--text-muted: #a8a399;--accent: #d97706;--accent-hover: #b45309;--accent-soft: rgba(217, 119, 6, .08);--accent-glow: rgba(217, 119, 6, .2);--green: #16a34a;--amber: #ca8a04;--red: #dc2626;--green-soft: rgba(22, 163, 74, .08);--amber-soft: rgba(202, 138, 4, .08);--red-soft: rgba(220, 38, 38, .08);--ic-claude: #c2410c;--ic-codex: #6366f1;--ic-repoburg: #059669;--ic-cursor: #db2777;--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;--font-mono: "JetBrains Mono", "SF Mono", "Fira Code", monospace;--r-sm: 4px;--r-md: 6px;--r-lg: 8px;--r-xl: 12px;--r-pill: 9999px;--ease: cubic-bezier(.25, .46, .45, .94);--ease-out: cubic-bezier(.22, 1, .36, 1);--dur-fast: .1s;--dur-normal: .15s;--dur-slow: .25s;--inset-highlight: inset 0 1px 0 rgba(0, 0, 0, .03);--inset-strong: inset 0 1px 0 rgba(0, 0, 0, .05);--focus-ring: 0 0 0 3px var(--accent-glow)}*{margin:0;padding:0;box-sizing:border-box}html,body{height:100%;overflow:hidden}body{font-family:var(--font-sans);font-size:13px;font-weight:400;background:var(--canvas);color:var(--text-2);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizeLegibility;font-feature-settings:"ss03","cv11";font-variant-numeric:tabular-nums;letter-spacing:-.01em}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:var(--surface-3);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--border-hover)}:focus-visible{outline:none;box-shadow:var(--focus-ring);border-radius:var(--r-md)}.ide-root{display:flex;flex-direction:column;height:100vh}.ide-layout{display:grid;grid-template-columns:240px 6px 1fr 6px 300px;flex:1;overflow:hidden}.resize-handle{width:6px;cursor:col-resize;background:transparent;flex-shrink:0;transition:background var(--dur-fast) var(--ease)}.resize-handle:hover,.resize-handle.dragging{background:var(--border-hover)}body.is-resizing{-webkit-user-select:none;user-select:none}body.is-resizing *{cursor:col-resize!important}.ide-sidebar{background:var(--surface-0);border-right:1px solid var(--border);overflow-y:auto;display:flex;flex-direction:column}.sidebar-header{padding:10px 12px 8px;display:flex;align-items:center;justify-content:space-between}.sidebar-header-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted)}.sidebar-add{width:20px;height:20px;border-radius:var(--r-sm);border:none;background:transparent;color:var(--text-muted);cursor:pointer;font-size:15px;display:flex;align-items:center;justify-content:center;transition:all var(--dur-normal) var(--ease)}.sidebar-add:hover{background:#0000000f;color:var(--text)}.tree-section{margin-bottom:2px}.tree-section-header{padding:5px 8px;display:flex;align-items:center;gap:6px;font-size:11px;font-weight:590;color:var(--text-2);cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:var(--r-sm);transition:background var(--dur-fast) var(--ease)}.tree-section-header:hover{background:#0000000a}.tree-chevron{font-size:9px;color:var(--text-muted);width:10px;transition:transform var(--dur-normal) var(--ease)}.tree-section.collapsed .tree-chevron{transform:rotate(-90deg)}.tree-section.collapsed .tree-children{display:none}.tree-count{margin-left:auto;font-size:10px;background:var(--surface-2);padding:1px 6px;border-radius:var(--r-pill);color:var(--text-muted);font-variant-numeric:tabular-nums}.tree-children{padding-left:4px}.tree-item{padding:4px 8px 4px 26px;border-radius:var(--r-sm);font-size:12px;cursor:pointer;display:flex;align-items:center;gap:6px;color:var(--text-2);-webkit-user-select:none;user-select:none;transition:all var(--dur-fast) var(--ease);position:relative}.tree-item:hover{background:#0000000a;color:var(--text)}.tree-item.active{background:var(--accent-soft);color:var(--text)}.tree-item.active:before{content:"";position:absolute;left:0;top:4px;bottom:4px;width:2px;background:var(--accent);border-radius:var(--r-pill)}.tree-item .ri{width:16px;height:16px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:700;color:#fff;flex-shrink:0}.tree-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.sidebar-footer{margin-top:auto;padding:8px 12px;border-top:1px solid var(--border-muted);font-size:11px;color:var(--text-muted);display:flex;align-items:center;gap:6px;cursor:pointer}.sidebar-footer:hover{color:var(--text-2)}.ide-center{display:flex;flex-direction:column;background:var(--canvas);overflow:hidden;min-height:0}.ide-tabs{display:flex;background:var(--surface-0);border-bottom:1px solid var(--border);overflow-x:auto;flex-shrink:0;scrollbar-width:none}.ide-tabs::-webkit-scrollbar{display:none}.ide-tab{padding:8px 12px;font-size:12px;color:var(--text-3);border-right:1px solid var(--border-muted);cursor:pointer;display:flex;align-items:center;gap:6px;white-space:nowrap;-webkit-user-select:none;user-select:none;transition:all var(--dur-fast) var(--ease);position:relative}.ide-tab:hover{background:#00000008;color:var(--text-2)}.ide-tab.active{color:var(--text);background:var(--canvas)}.ide-tab.active:after{content:"";position:absolute;left:0;right:0;top:0;height:2px;background:var(--accent)}.ide-tab .ri{width:14px;height:14px;font-size:7px;border-radius:3px}.ide-tab-close{margin-left:4px;color:var(--text-muted);font-size:14px;opacity:0;transition:opacity var(--dur-fast) var(--ease);display:flex;align-items:center}.ide-tab:hover .ide-tab-close{opacity:1}.ide-tab-close:hover{color:var(--red)}.ide-tab-add{padding:8px 10px;color:var(--text-muted);cursor:pointer;font-size:14px;transition:color var(--dur-fast) var(--ease);display:flex;align-items:center}.ide-tab-add:hover{color:var(--text)}.ide-render{flex:1;overflow:hidden;display:flex;flex-direction:column;position:relative;min-height:0}.render-scroll{flex:1;padding:20px 28px;overflow-y:auto;font-size:14px;line-height:1.7;color:var(--text);min-height:0}.render-content{max-width:720px}.render-content h1,.render-content h2,.render-content h3{margin:20px 0 8px;font-weight:600;color:var(--text);line-height:1.3}.render-content h1{font-size:20px}.render-content h2{font-size:17px}.render-content h3{font-size:15px}.render-content p{margin-bottom:12px;color:var(--text-2)}.render-content ul,.render-content ol{margin-bottom:12px;padding-left:24px}.render-content li{margin-bottom:4px;color:var(--text-2)}.render-content code{font-family:var(--font-mono);font-size:12px;background:var(--surface-1);padding:2px 6px;border-radius:var(--r-sm);color:var(--accent)}.render-content pre{background:var(--surface-0);border:1px solid var(--border);border-radius:var(--r-md);padding:12px 16px;overflow-x:auto;margin-bottom:12px}.render-content pre code{background:none;padding:0;color:var(--text);font-size:12.5px;line-height:1.6}.render-content a{color:var(--accent);text-decoration:none}.render-content a:hover{text-decoration:underline}.render-content blockquote{border-left:3px solid var(--border);padding-left:14px;color:var(--text-3);margin-bottom:12px}.render-content table{width:100%;border-collapse:collapse;margin-bottom:12px}.render-content th,.render-content td{border:1px solid var(--border);padding:6px 10px;font-size:13px}.render-content th{background:var(--surface-1);font-weight:510}.turn-header{display:flex;align-items:center;gap:8px;margin-bottom:14px}.turn-header-icon{width:24px;height:24px;border-radius:var(--r-sm);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:9px;font-weight:700;color:#fff}.turn-header-title{font-size:14px;font-weight:600;color:var(--text)}.turn-header-meta{font-size:11px;color:var(--text-muted)}.tool-calls{margin-bottom:16px}.tool-calls-header{display:flex;align-items:center;gap:8px;padding:6px 12px;background:var(--surface-1);border:1px solid var(--border);border-radius:var(--r-sm);cursor:pointer;transition:background .15s ease-out;-webkit-user-select:none;user-select:none}.tool-calls-header:hover{background:var(--surface-2)}.tool-calls-icon{display:flex;align-items:center;color:var(--text-3);flex-shrink:0}.tool-calls-summary{font-size:13px;font-weight:500;color:var(--text-2);flex:1}.tool-calls-chevron{color:var(--text-muted);transition:transform .15s ease-out;flex-shrink:0}.tool-calls-body{margin-top:8px;padding-left:16px;border-left:2px solid var(--border)}.intermediate-segment{margin-bottom:16px}.intermediate-segment:last-child{margin-bottom:0}.user-message{font-size:14px;color:var(--text);background:var(--accent-soft);padding:12px 16px;border-radius:var(--r-md);border:1px solid rgba(217,119,6,.15)}.cmd-bar{position:relative;display:flex;flex-direction:column;padding:0;background:var(--surface-0);border-top:1px solid var(--border);flex-shrink:0}.cmd-input-row{display:flex;align-items:flex-end;gap:8px;padding:8px 12px}.cmd-bar:focus-within{box-shadow:inset 0 0 0 1px #d9770666}.cmd-textarea{flex:1;border:none;background:transparent;color:var(--text);font-size:13px;font-family:var(--font-sans);outline:none;resize:none;overflow-y:auto;line-height:1.5;padding:6px 0}.cmd-textarea::placeholder{color:var(--text-muted)}.cmd-textarea:disabled{opacity:.5}.cmd-textarea:focus-visible{box-shadow:none}.cmd-send-btn{width:28px;height:28px;border:none;border-radius:var(--r-sm);background:var(--surface-2);color:var(--text-muted);cursor:default;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all var(--dur-fast) var(--ease)}.cmd-send-btn:not(:disabled){background:var(--accent);color:#fff;cursor:pointer}.cmd-send-btn:not(:disabled):hover{background:var(--accent-hover)}.cmd-send-btn:disabled{opacity:.4}.cmd-stop-btn{width:28px;height:28px;border:none;border-radius:var(--r-sm);background:var(--red);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all var(--dur-fast) var(--ease)}.cmd-stop-btn:hover{background:#b91c1c}.ide-right{background:var(--surface-0);border-left:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}.right-tabs{display:flex;padding:0 8px;border-bottom:1px solid var(--border);flex-shrink:0;gap:2px}.right-tab{padding:7px 10px;font-size:11px;font-weight:510;color:var(--text-3);cursor:pointer;border-bottom:2px solid transparent;-webkit-user-select:none;user-select:none;transition:all var(--dur-fast) var(--ease);margin-bottom:-1px}.right-tab:hover{color:var(--text-2)}.right-tab.active{color:var(--text);border-bottom-color:var(--accent)}.right-content{flex:1;overflow-y:auto;padding:14px}.right-section{margin-bottom:20px}.right-section-title{font-size:10px;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);margin-bottom:8px}.turn-item{display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:var(--r-sm);cursor:pointer;font-size:12px;color:var(--text-3);transition:all var(--dur-fast) var(--ease);position:relative}.turn-item:hover{background:#0000000a;color:var(--text-2)}.turn-item.active{background:var(--accent-soft);color:var(--text)}.turn-item.active:before{content:"";position:absolute;left:0;top:4px;bottom:4px;width:2px;background:var(--accent);border-radius:var(--r-pill)}.turn-item-icon{width:18px;height:18px;border-radius:var(--r-sm);display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:8px;font-weight:700;color:#fff}.turn-item-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.turn-item-status{font-size:10px;color:var(--amber);flex-shrink:0}.turn-item-meta{font-size:10px;color:var(--text-muted);flex-shrink:0}.file-change{display:flex;align-items:center;gap:8px;padding:5px 0;font-size:12px;border-bottom:1px solid var(--border-muted)}.file-change:last-child{border:none}.file-change.clickable{cursor:pointer;transition:background var(--dur-fast) var(--ease)}.file-change.clickable:hover{background:#00000008}.fc-status{width:14px;text-align:center;font-weight:700;font-size:11px;flex-shrink:0}.fc-add{color:var(--green)}.fc-mod{color:var(--amber)}.fc-del{color:var(--red)}.fc-name{flex:1;color:var(--text-2);font-family:var(--font-mono);font-size:11px}.fc-delta{font-size:10px;color:var(--text-muted);flex-shrink:0;font-variant-numeric:tabular-nums}.diff-file-header{display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--surface-0);border:1px solid var(--border);border-radius:var(--r-md) var(--r-md) 0 0;border-bottom:none}.diff-file-status{width:16px;height:16px;border-radius:3px;display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;flex-shrink:0}.diff-file-name{font-family:var(--font-mono);font-size:13px;font-weight:510;color:var(--text)}.diff-file-stats{display:flex;align-items:center;gap:6px;margin-left:auto;font-size:11px;font-variant-numeric:tabular-nums}.diff-file-badge{padding:1px 7px;border-radius:var(--r-pill);background:var(--surface-2);color:var(--text-3);font-size:10px}.diff-body{border:1px solid var(--border);border-radius:0 0 var(--r-md) var(--r-md);overflow:hidden;margin-bottom:20px}.diff-line{display:flex;align-items:flex-start;font-family:var(--font-mono);font-size:12px;line-height:1.6}.diff-line-num{width:42px;flex-shrink:0;text-align:right;padding:0 8px;color:var(--text-muted);background:var(--surface-0);-webkit-user-select:none;user-select:none;border-right:1px solid var(--border-muted)}.diff-sign{width:20px;flex-shrink:0;text-align:center;-webkit-user-select:none;user-select:none;font-weight:700}.diff-line-content{flex:1;padding:0 8px;white-space:pre-wrap;word-break:break-all;min-width:0}.diff-line-add{background:#16a34a0f}.diff-line-add .diff-sign{color:var(--green)}.diff-line-del{background:#dc26260f}.diff-line-del .diff-sign{color:var(--red)}.diff-line-ctx .diff-sign{color:var(--text-muted)}.diff-line-ctx .diff-line-content{color:var(--text-3)}.changes-scope-toggle{display:flex;gap:2px;margin-bottom:12px;padding:2px;background:var(--surface-1);border-radius:var(--r-md)}.scope-btn{flex:1;padding:5px 10px;border:none;background:transparent;color:var(--text-3);font-size:11px;font-weight:510;cursor:pointer;border-radius:var(--r-sm);font-family:var(--font-sans);transition:all var(--dur-fast) var(--ease)}.scope-btn:hover{color:var(--text-2)}.scope-btn.active{background:var(--surface-0);color:var(--text);box-shadow:var(--inset-highlight)}.ide-statusbar{display:flex;align-items:center;gap:12px;padding:0 12px;height:24px;background:var(--surface-0);border-top:1px solid var(--border);font-size:11px;color:var(--text-muted);flex-shrink:0;font-variant-numeric:tabular-nums}.sb-item{display:flex;align-items:center;gap:3px;cursor:pointer;transition:color var(--dur-fast) var(--ease)}.sb-item:hover{color:var(--text-2)}.sb-spacer{flex:1}.sb-divider{color:var(--border)}.sb-context{display:flex;align-items:center;gap:5px}.sb-context-text{font-size:10px;color:var(--text-muted);font-variant-numeric:tabular-nums}.model-selector{position:relative}.model-selector-btn{display:flex;align-items:center;gap:4px;background:none;border:none;cursor:pointer;color:var(--text-muted);font-size:11px;padding:2px 6px;border-radius:3px;transition:background var(--dur-fast) var(--ease),color var(--dur-fast) var(--ease)}.model-selector-btn:hover{background:var(--surface-1);color:var(--text-2)}.model-selector-provider{font-weight:500;color:var(--accent)}.model-selector-sep{color:var(--border);margin:0 1px}.model-selector-model{color:var(--text-2)}.model-selector-reasoning{color:var(--text-muted);font-style:italic}.model-selector-chevron{opacity:.5;flex-shrink:0}.model-selector-popover{position:absolute;bottom:calc(100% + 4px);left:0;display:flex;gap:1px;background:var(--surface-0);border:1px solid var(--border);border-radius:6px;box-shadow:0 4px 16px #0003;z-index:100;min-width:0;overflow:hidden}.model-selector-column{min-width:140px;max-width:220px;max-height:280px;overflow-y:auto;padding:4px 0;background:var(--surface-0)}.model-selector-col-header{position:sticky;top:0;background:var(--surface-0);padding:4px 10px;font-size:9px;font-weight:600;text-transform:uppercase;color:var(--text-muted);letter-spacing:.5px}.model-selector-item{display:flex;align-items:center;gap:6px;padding:6px 10px;cursor:pointer;font-size:11px;color:var(--text-2);transition:background var(--dur-fast) var(--ease)}.model-selector-item:hover{background:var(--surface-1)}.model-selector-item.selected{color:var(--accent)}.model-selector-item.disabled{cursor:default;opacity:.5}.model-selector-item.disabled:hover{background:none}.model-selector-item-main{display:flex;flex-direction:column;gap:1px;flex:1;min-width:0}.model-selector-item-name{font-weight:500;white-space:nowrap}.model-selector-item-desc{font-size:9px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.model-selector-coming-soon{display:flex;align-items:center;gap:2px;font-size:9px;color:var(--text-muted);font-style:italic}.model-selector-empty{padding:8px 10px;font-size:10px;color:var(--text-muted);font-style:italic}.dialog-select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 8px center;padding-right:26px}.badge{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;border-radius:var(--r-pill);font-size:11px;font-weight:510;white-space:nowrap}.badge-running{background:var(--green-soft);color:var(--green)}.badge-queued{background:var(--amber-soft);color:var(--amber)}.badge-done{background:var(--accent-soft);color:var(--accent)}.badge-failed{background:var(--red-soft);color:var(--red)}.badge-paused{background:#a8a3991f;color:var(--text-muted)}.runtime-icon{width:20px;height:20px;border-radius:var(--r-sm);display:flex;align-items:center;justify-content:center;font-size:9px;font-weight:700;color:#fff;flex-shrink:0;letter-spacing:0}.ic-claude{background:var(--ic-claude)}.ic-codex{background:var(--ic-codex)}.ic-repoburg{background:var(--ic-repoburg)}.ic-cursor{background:var(--ic-cursor)}.status-dot{width:6px;height:6px;border-radius:50%;display:inline-block;flex-shrink:0}.status-dot.running{background:var(--green);animation:dot-pulse 1.5s ease-in-out infinite}.status-dot.queued{background:var(--amber)}.status-dot.error{background:var(--red)}.status-dot.done{background:var(--accent)}.status-dot.paused{background:var(--text-muted)}[data-lucide],svg.lucide{width:16px;height:16px;stroke-width:1.5;flex-shrink:0}.icon-xs{width:12px!important;height:12px!important}.icon-sm{width:14px!important;height:14px!important}.icon-lg{width:20px!important;height:20px!important}.dialog-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#0000004d;display:flex;align-items:center;justify-content:center;z-index:100}.dialog{background:var(--surface-0);border:1px solid var(--border);border-radius:var(--r-lg);width:400px;max-width:90vw;box-shadow:0 8px 32px #00000029}.dialog-header{display:flex;align-items:center;justify-content:space-between;padding:14px 16px;border-bottom:1px solid var(--border-muted)}.dialog-title{font-size:14px;font-weight:600;color:var(--text)}.dialog-close{border:none;background:transparent;color:var(--text-muted);cursor:pointer;padding:4px;border-radius:var(--r-sm);display:flex;align-items:center;transition:all var(--dur-fast) var(--ease)}.dialog-close:hover{background:#0000000f;color:var(--text)}.dialog-body{padding:16px;display:flex;flex-direction:column;gap:14px}.dialog-label{display:flex;flex-direction:column;gap:4px;font-size:11px;font-weight:510;color:var(--text-muted)}.dialog-input{border:1px solid var(--border);border-radius:var(--r-sm);padding:8px 10px;font-size:13px;font-family:var(--font-sans);color:var(--text);background:var(--canvas);outline:none;transition:border-color var(--dur-fast) var(--ease)}.dialog-input:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}.dialog-footer{display:flex;justify-content:flex-end;gap:8px;padding:12px 16px;border-top:1px solid var(--border-muted)}.dialog-btn-cancel{padding:6px 14px;border-radius:var(--r-sm);border:1px solid var(--border);background:transparent;color:var(--text-2);font-size:12px;cursor:pointer;font-family:var(--font-sans);transition:all var(--dur-fast) var(--ease)}.dialog-btn-cancel:hover{background:var(--surface-2);color:var(--text)}.dialog-btn-create{padding:6px 14px;border-radius:var(--r-sm);border:none;background:var(--accent);color:#fff;font-size:12px;font-weight:510;cursor:pointer;font-family:var(--font-sans);transition:all var(--dur-fast) var(--ease)}.dialog-btn-create:hover{background:var(--accent-hover)}.turn-group{margin-bottom:24px}.turn-block{margin-bottom:32px;padding-bottom:8px;border-bottom:1px solid var(--border)}.turn-block:last-child{border-bottom:none}.cmd-block{background:var(--surface-0);border:1px solid var(--border);border-radius:var(--r-md);padding:10px 14px;overflow-x:auto;margin-bottom:12px}.cmd-block code{font-family:var(--font-mono);font-size:12px;color:var(--text)}.cmd-output{background:var(--surface-0);border:1px solid var(--border-muted);border-radius:var(--r-md);padding:10px 14px;overflow-x:auto;margin-bottom:12px;max-height:200px;overflow-y:auto}.cmd-output code{font-family:var(--font-mono);font-size:12px;color:var(--text-3)}.cmd-output-toggle{border:1px solid var(--border);border-radius:var(--r-sm);background:var(--surface-1);color:var(--text-muted);font-size:11px;font-family:var(--font-sans);padding:4px 10px;cursor:pointer;margin-bottom:12px;transition:all var(--dur-fast) var(--ease)}.cmd-output-toggle:hover{background:var(--surface-2);color:var(--text-2)}.mcp-args{margin-bottom:12px}.mcp-arg{margin-bottom:6px}.mcp-arg-key{font-size:11px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.04em;display:block;margin-bottom:3px}.mcp-arg-value{font-size:12px;color:var(--text-2);white-space:pre-wrap;word-break:break-word}.file-change-list{display:flex;flex-direction:column;gap:2px}.todo-list{list-style:none;padding-left:0}.todo-done{color:var(--text-3);text-decoration:line-through}.todo-pending{color:var(--text-2)}.empty-state{padding:40px 20px;text-align:center;color:var(--text-muted);font-size:13px}.palette-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000040;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);display:flex;align-items:flex-start;justify-content:center;padding-top:120px;z-index:200}.palette{background:var(--surface-0);border:1px solid var(--border);border-radius:var(--r-lg);width:520px;max-width:90vw;box-shadow:0 12px 40px #0003;overflow:hidden}.palette-search{display:flex;align-items:center;gap:8px;padding:12px 16px;border-bottom:1px solid var(--border-muted)}.palette-search input{flex:1;border:none;background:transparent;color:var(--text);font-size:15px;font-family:var(--font-sans);outline:none}.palette-search input::placeholder{color:var(--text-muted)}.palette-esc{font-size:10px;font-weight:600;color:var(--text-muted);background:var(--surface-2);padding:2px 6px;border-radius:var(--r-sm)}.palette-list{max-height:360px;overflow-y:auto;padding:4px}.palette-empty{padding:24px;text-align:center;color:var(--text-muted);font-size:13px}.palette-item{display:flex;align-items:center;gap:10px;padding:8px 12px;border-radius:var(--r-sm);cursor:pointer;transition:background var(--dur-fast) var(--ease)}.palette-item.selected{background:var(--accent-soft)}.palette-item-icon{color:var(--text-muted);flex-shrink:0;display:flex;align-items:center}.palette-item.selected .palette-item-icon{color:var(--accent)}.palette-item-label{font-size:13px;color:var(--text);font-weight:510}.palette-item-hint{font-size:11px;color:var(--text-muted);margin-left:auto}.palette-item-enter{color:var(--text-muted);flex-shrink:0}.toast-container{position:fixed;bottom:36px;right:16px;display:flex;flex-direction:column-reverse;gap:8px;z-index:300;pointer-events:none}.toast{display:flex;align-items:center;gap:8px;padding:10px 14px;border-radius:var(--r-md);font-size:12px;font-weight:500;min-width:280px;max-width:400px;pointer-events:auto;animation:toast-in .2s ease-out;border:1px solid var(--border);background:var(--surface-0);box-shadow:0 4px 16px #0000001f}@keyframes toast-in{0%{opacity:0;transform:translate(20px)}to{opacity:1;transform:translate(0)}}.toast-success{border-left:3px solid var(--green)}.toast-success svg{color:var(--green)}.toast-error{border-left:3px solid var(--red)}.toast-error svg{color:var(--red)}.toast-info{border-left:3px solid var(--accent)}.toast-info svg{color:var(--accent)}.toast-warn{border-left:3px solid var(--amber)}.toast-warn svg{color:var(--amber)}.toast-message{flex:1;color:var(--text-2)}.toast-close{border:none;background:transparent;color:var(--text-muted);cursor:pointer;padding:2px;border-radius:var(--r-sm);display:flex;align-items:center;transition:color var(--dur-fast) var(--ease)}.toast-close:hover{color:var(--text)}.error-banner{display:flex;align-items:center;gap:10px;padding:10px 16px;background:#dc262614;border:1px solid rgba(220,38,38,.2);border-radius:var(--r-md);margin-bottom:16px}.banner-icon{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0}.error-banner .banner-icon{background:var(--red);color:#fff}.error-banner .banner-text{color:var(--red);font-size:13px;font-weight:500}.aborted-banner{display:flex;align-items:center;gap:10px;padding:10px 16px;background:#d9770614;border:1px solid rgba(217,119,6,.2);border-radius:var(--r-md);margin-bottom:16px}.aborted-banner .banner-icon{background:var(--amber);color:#fff}.aborted-banner .banner-text{color:var(--amber);font-size:13px;font-weight:500}@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important}}.skeleton-bars{width:80%;max-width:500px;margin:20px 0}.skeleton-bar{height:10px;border-radius:var(--r-sm);background:var(--surface-1);margin-bottom:8px;animation:shimmer 1.2s infinite}.skeleton-bar:nth-child(1){width:60%}.skeleton-bar:nth-child(2){width:85%}.skeleton-bar:nth-child(3){width:70%}.skeleton-bar:nth-child(4){width:90%}.skeleton-bar:nth-child(5){width:50%}@keyframes shimmer{0%{opacity:.3}50%{opacity:.6}to{opacity:.3}}@keyframes dot-pulse{0%{opacity:1}to{opacity:.4}}.diff-file-header{transition:background .3s ease-out}.diff-file-header.diff-highlight{background:var(--accent-soft)}.palette-footer{display:flex;gap:16px;padding:8px 16px;border-top:1px solid var(--border-muted);font-size:11px;color:var(--text-muted)}.palette-footer-hint{display:flex;align-items:center;gap:4px}.kbd{font-family:var(--font-mono);font-size:10px;padding:1px 5px;border-radius:var(--r-sm);background:var(--surface-2);border:1px solid var(--border)}.toast-undo{font-size:12px;font-weight:500;color:var(--accent);background:transparent;border:none;cursor:pointer;padding:2px 8px;border-radius:var(--r-sm);display:flex;align-items:center;gap:4px;transition:background var(--dur-fast) var(--ease)}.toast-undo:hover{background:var(--accent-soft)}.tree-section-label{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}.tree-section-add{width:18px;height:18px;border:none;border-radius:var(--r-sm);background:transparent;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all var(--dur-fast) var(--ease);flex-shrink:0}.tree-section-add:hover{background:var(--accent-soft);color:var(--accent)}.tree-empty{padding:4px 8px 4px 26px;font-size:11px;color:var(--text-muted);font-style:italic}.tree-subgroup{margin-bottom:1px}.tree-subgroup-header{padding:3px 8px 3px 18px;display:flex;align-items:center;gap:5px;font-size:10px;font-weight:590;color:var(--text-muted);cursor:pointer;-webkit-user-select:none;user-select:none;border-radius:var(--r-sm);transition:background var(--dur-fast) var(--ease)}.tree-subgroup-header:hover{background:#0000000a;color:var(--text-2)}.tree-subgroup.collapsed .tree-chevron{transform:rotate(-90deg)}.tree-subgroup.collapsed .tree-subgroup-items{display:none}.tree-subgroup-label{text-transform:uppercase;letter-spacing:.05em}.tree-subgroup-count{margin-left:auto;font-size:9px;color:var(--text-muted);font-variant-numeric:tabular-nums}.tree-subgroup-items{padding-left:0}.tree-item-finished .tree-label{color:var(--text-muted);opacity:.6}.tree-item-finished:hover .tree-label{opacity:.85}.ctx-menu{background:var(--surface-0);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:0 4px 16px #0000001f;padding:4px;min-width:160px}.ctx-menu-item{display:flex;align-items:center;gap:8px;padding:6px 10px;border-radius:var(--r-sm);font-size:12px;color:var(--text-2);cursor:pointer;transition:background var(--dur-fast) var(--ease)}.ctx-menu-item:hover{background:var(--accent-soft);color:var(--text)}.ctx-menu-item.ctx-danger:hover{background:#dc262614;color:var(--red)}.ctx-menu-item.ctx-disabled{opacity:.4;cursor:default}.ctx-menu-item.ctx-disabled:hover{background:transparent;color:var(--text-2)}.tree-rename-input{flex:1;border:1px solid var(--accent);border-radius:var(--r-sm);padding:1px 6px;font-size:12px;font-family:var(--font-sans);color:var(--text);background:var(--canvas);outline:none;box-shadow:var(--focus-ring);min-width:0}.slash-popup{position:absolute;bottom:100%;left:12px;right:12px;margin-bottom:4px;background:var(--surface-0);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:0 4px 16px #0000001f;max-height:280px;overflow-y:auto;z-index:100}.slash-popup-header{padding:6px 10px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:4px}.slash-popup-back{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:11px;padding:2px 4px;border-radius:var(--r-sm)}.slash-popup-back:hover{background:var(--surface-1);color:var(--text-2)}.slash-popup-empty{padding:12px 10px;font-size:12px;color:var(--text-muted);text-align:center}.slash-popup-item{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 10px;cursor:pointer;border-radius:var(--r-sm);transition:background var(--dur-fast) var(--ease)}.slash-popup-item:hover,.slash-popup-item.selected{background:var(--accent-soft)}.slash-popup-label{font-size:12px;font-weight:500;color:var(--text-2);font-family:var(--font-mono)}.slash-popup-hint{font-size:11px;color:var(--text-muted)}.slash-popup-item-main{display:flex;flex-direction:column;gap:1px}.slash-popup-check{color:var(--accent);font-size:13px}.skill-badges{display:flex;flex-wrap:wrap;gap:4px;padding:6px 12px 0}.skill-badge{display:inline-flex;align-items:center;gap:4px;padding:2px 6px;font-size:11px;font-family:var(--font-mono);color:var(--accent);background:var(--accent-soft);border-radius:var(--r-sm)}.skill-badge-remove{background:none;border:none;color:var(--text-muted);cursor:pointer;font-size:13px;line-height:1;padding:0}.skill-badge-remove:hover{color:var(--red)}.attachment-preview-area{display:flex;flex-wrap:wrap;gap:4px;padding:6px 12px 0}.attachment-chip{display:inline-flex;align-items:center;gap:4px;padding:2px 6px 2px 2px;border:1px solid var(--border);border-radius:var(--r-pill);background:var(--surface-1);max-width:180px;position:relative;transition:border-color var(--dur-fast) var(--ease)}.attachment-chip:hover{border-color:var(--accent)}.attachment-thumb{width:18px;height:18px;object-fit:cover;border-radius:50%;flex-shrink:0}.attachment-file-icon{width:14px;height:14px;color:var(--text-muted);flex-shrink:0}.attachment-info{display:flex;flex-direction:column;gap:0;overflow:hidden;min-width:0}.attachment-name{font-size:10px;font-family:var(--font-mono);color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100px}.attachment-size{font-size:9px;color:var(--text-muted);font-variant-numeric:tabular-nums}.attachment-remove{background:none;border:none;color:var(--text-muted);cursor:pointer;padding:1px;display:flex;align-items:center;justify-content:center;border-radius:var(--r-xs);transition:all var(--dur-fast) var(--ease);flex-shrink:0}.attachment-remove:hover{color:var(--red);background:#ef44441a}.attachment-clip-btn{width:28px;height:28px;border:none;border-radius:var(--r-sm);background:transparent;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all var(--dur-fast) var(--ease)}.attachment-clip-btn:hover{color:var(--accent);background:var(--surface-2)}.attachment-clip-btn:disabled{opacity:.3;cursor:default}.cmd-bar.drag-over{box-shadow:inset 0 0 0 2px var(--accent);background:var(--accent-soft)}.image-preview-modal{position:fixed;top:0;right:0;bottom:0;left:0;z-index:10000;background:#000c;display:flex;align-items:center;justify-content:center;padding:32px;animation:modal-fade-in .15s ease-out}@keyframes modal-fade-in{0%{opacity:0}to{opacity:1}}.image-preview-content{position:relative;max-width:90vw;max-height:90vh;display:flex;flex-direction:column;align-items:center;gap:8px}.image-preview-content img{max-width:100%;max-height:80vh;object-fit:contain;border-radius:var(--r-sm);border:1px solid var(--border)}.image-preview-info{display:flex;gap:12px;font-size:12px;color:var(--text-muted);font-family:var(--font-mono)}.image-preview-close{position:absolute;top:-12px;right:-12px;width:28px;height:28px;border:none;border-radius:50%;background:var(--surface-2);color:var(--text);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all var(--dur-fast) var(--ease)}.image-preview-close:hover{background:var(--red);color:#fff}.attachment-pills{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}.attachment-pill{display:inline-flex;align-items:center;gap:4px;padding:2px 8px 2px 2px;border:1px solid var(--border);border-radius:var(--r-pill);background:var(--surface-1);font-size:10px;font-family:var(--font-mono);color:var(--text-muted);transition:border-color var(--dur-fast) var(--ease)}.attachment-pill:hover{border-color:var(--accent)}.attachment-pill-thumb{width:16px;height:16px;object-fit:cover;border-radius:50%;flex-shrink:0}.attachment-pill-icon{color:var(--text-muted);flex-shrink:0}.attachment-pill-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100px}.right-tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:16px;height:16px;padding:0 4px;margin-left:4px;font-size:10px;font-weight:600;color:#fff;background:var(--accent);border-radius:8px}.task-count-badge{font-size:10px;font-weight:500}.task-card{border:1px solid var(--border);border-radius:var(--r-md);padding:10px 12px;margin-bottom:10px;transition:border-color var(--dur-fast) var(--ease)}.task-card.task-running{border-color:var(--accent)}.task-card.task-failed{border-color:var(--red)}.task-card-header{display:flex;align-items:center;gap:8px}.task-card-icon{display:flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:var(--r-sm);background:var(--surface-2);color:var(--text-3);flex-shrink:0}.task-card-type{font-size:13px;font-weight:600;color:var(--text)}.task-card-status{display:flex;align-items:center;gap:3px;font-size:11px;padding:2px 6px;border-radius:var(--r-sm);flex-shrink:0}.task-status-running{color:var(--accent);background:var(--accent-soft)}.task-status-done{color:var(--green);background:var(--green-soft)}.task-status-failed{color:var(--red);background:var(--red-soft)}.task-card-chevron{color:var(--text-muted);flex-shrink:0;transition:transform var(--dur-fast) var(--ease)}.task-card-desc{font-size:12px;color:var(--text-2);margin-top:6px;font-family:var(--font-mono)}.task-card-meta{font-size:11px;color:var(--text-muted);margin-top:4px}.task-card-body{margin-top:8px}.task-output{max-height:300px}.task-card-placeholder{font-size:12px;color:var(--text-muted);font-style:italic;padding:8px 0}.task-card-context{font-size:10px;color:var(--text-muted);margin-top:6px;padding-top:6px;border-top:1px solid var(--border-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}mark[data-thread-id]{background:#d977062e;border-radius:2px;cursor:pointer;padding:0 1px;transition:background var(--dur-fast) var(--ease)}mark[data-thread-id]:hover{background:#d977064d}mark[data-thread-id].thread-mark-resolved{background:transparent;border-bottom:2px dashed var(--text-muted)}.selection-actions{position:fixed;z-index:9999;display:flex;gap:4px}.discuss-button{display:flex;align-items:center;gap:4px;padding:4px 10px;border:1px solid var(--border);border-radius:var(--r-sm);background:var(--surface-0);color:var(--accent);font-size:11px;font-weight:500;cursor:pointer;box-shadow:0 2px 8px #0000001f;transition:all var(--dur-fast) var(--ease)}.discuss-button:hover{background:var(--accent-soft);border-color:var(--accent)}.thread-create-popover{position:fixed;z-index:9999;min-width:280px;max-width:360px;background:var(--surface-0);border:1px solid var(--border);border-radius:var(--r-md);box-shadow:0 4px 16px #00000026;padding:10px;display:flex;flex-direction:column;gap:8px;transform:translate(-100%)}.thread-create-header{display:flex;align-items:flex-start;justify-content:space-between;gap:8px}.thread-create-quote{font-size:11px;color:var(--text-muted);font-style:italic;flex:1;line-height:1.4}.thread-create-close{border:none;background:transparent;color:var(--text-muted);cursor:pointer;padding:2px;border-radius:var(--r-sm);flex-shrink:0}.thread-create-close:hover{background:var(--surface-1);color:var(--text)}.thread-create-input{border:1px solid var(--border);border-radius:var(--r-sm);padding:8px 10px;font-size:13px;font-family:var(--font-sans);color:var(--text);background:var(--canvas);outline:none;resize:none;line-height:1.5;transition:border-color var(--dur-fast) var(--ease)}.thread-create-input:focus{border-color:var(--accent);box-shadow:var(--focus-ring)}.thread-create-footer{display:flex;align-items:center;justify-content:space-between}.thread-create-hint{font-size:10px;color:var(--text-muted)}.thread-create-send{display:flex;align-items:center;gap:4px;padding:4px 10px;border:none;border-radius:var(--r-sm);background:var(--accent);color:#fff;font-size:11px;font-weight:500;cursor:pointer;transition:background var(--dur-fast) var(--ease)}.thread-create-send:disabled{opacity:.4;cursor:default}.thread-create-send:not(:disabled):hover{background:var(--accent-hover)}.thread-filter-tabs{display:flex;gap:2px;margin-bottom:12px;padding:2px;background:var(--surface-1);border-radius:var(--r-md)}.thread-filter-tab{flex:1;padding:4px 8px;border:none;background:transparent;color:var(--text-3);font-size:10px;font-weight:500;cursor:pointer;border-radius:var(--r-sm);font-family:var(--font-sans);transition:all var(--dur-fast) var(--ease)}.thread-filter-tab:hover{color:var(--text-2)}.thread-filter-tab.active{background:var(--surface-0);color:var(--text);box-shadow:var(--inset-highlight)}.thread-card{border:1px solid var(--border);border-radius:var(--r-md);padding:10px 12px;margin-bottom:10px;transition:border-color var(--dur-fast) var(--ease),background var(--dur-fast) var(--ease)}.thread-card.thread-resolved{opacity:.7}.thread-card-highlighted{border-color:var(--accent);background:var(--accent-soft)}.thread-card-header{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;cursor:pointer}.thread-card-quote{font-size:11px;color:var(--text-muted);font-style:italic;line-height:1.4;flex:1}.thread-card-header-right{display:flex;align-items:center;gap:4px;flex-shrink:0}.thread-status-badge{font-size:10px;font-weight:600;padding:2px 8px;border-radius:var(--r-pill)}.thread-status-open{background:var(--accent-soft);color:var(--accent)}.thread-status-resolved{background:var(--green-soft);color:var(--green)}.thread-card-conversation{margin-top:8px;max-height:300px;overflow-y:auto}.thread-card-turn{margin-bottom:8px;padding-bottom:8px;border-bottom:1px solid var(--border-muted)}.thread-card-turn:last-child{border-bottom:none;margin-bottom:0;padding-bottom:0}.thread-card-user-message{font-size:12px;color:var(--text);background:var(--accent-soft);padding:6px 10px;border-radius:var(--r-sm);border:1px solid rgba(217,119,6,.15);margin-bottom:4px}.thread-card-ai-response{font-size:12px;color:var(--text-2);padding:4px 0;line-height:1.5}.thread-card-ai-response p{margin-bottom:6px}.thread-card-ai-response code{font-family:var(--font-mono);font-size:11px;background:var(--surface-1);padding:1px 4px;border-radius:var(--r-sm);color:var(--accent)}.thread-card-ai-response pre{background:var(--surface-0);border:1px solid var(--border);border-radius:var(--r-sm);padding:8px;overflow-x:auto;margin-bottom:6px}.thread-card-ai-response pre code{background:none;padding:0}.thread-card-pending{font-size:11px;color:var(--text-muted);font-style:italic;padding:4px 0}.thread-card-placeholder{font-size:11px;color:var(--text-muted);font-style:italic;padding:8px 0}.thread-card-followup{display:flex;gap:4px;margin-top:8px}.thread-card-followup-input{flex:1;border:1px solid var(--border);border-radius:var(--r-sm);padding:6px 8px;font-size:12px;font-family:var(--font-sans);color:var(--text);background:var(--canvas);outline:none;resize:none;line-height:1.5;transition:border-color var(--dur-fast) var(--ease)}.thread-card-followup-input:focus{border-color:var(--accent)}.thread-card-followup-input:disabled{opacity:.5}.thread-card-followup-send{width:28px;height:28px;border:none;border-radius:var(--r-sm);background:var(--accent);color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background var(--dur-fast) var(--ease)}.thread-card-followup-send:disabled{opacity:.4;cursor:default}.thread-card-followup-send:not(:disabled):hover{background:var(--accent-hover)}.thread-locate-btn{background:none;border:none;color:var(--text-muted);cursor:pointer;padding:2px;border-radius:3px;display:flex;align-items:center;transition:all var(--dur-fast) var(--ease);flex-shrink:0}.thread-locate-btn:hover{color:var(--accent);background:var(--accent-soft)}.thread-mark-flash{animation:mark-flash 1.5s ease-out}@keyframes mark-flash{0%{background:#d9770680}to{background:#d977062e}}.thread-card-actions{display:flex;gap:6px;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-muted)}.thread-action-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;border:1px solid var(--border);border-radius:var(--r-sm);background:transparent;color:var(--text-3);font-size:11px;cursor:pointer;font-family:var(--font-sans);transition:all var(--dur-fast) var(--ease)}.thread-action-btn:hover{background:var(--surface-1);color:var(--text)}.thread-action-btn.thread-action-danger:hover{background:#dc262614;color:var(--red);border-color:#dc262633}.agent-call-card{display:flex;align-items:center;gap:8px;padding:8px 14px;border-left:3px solid var(--accent);background:var(--surface);border-radius:0 var(--r-sm) var(--r-sm) 0}.agent-call-icon{color:var(--text-2);display:flex;align-items:center}.agent-call-desc{font-size:13px;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.agent-call-status{font-size:10px;font-family:var(--font-mono);padding:1px 6px;border-radius:var(--r-sm)}.agent-call-status.running{color:var(--accent);background:var(--accent-soft)}.agent-call-status.done{color:var(--green);background:#22c55e14}.agent-call-status.failed{color:var(--red);background:#ef444414}.ask-user-card{border-left:3px solid var(--accent)}.ask-user-groups{display:flex;flex-direction:column;gap:10px;margin-top:6px}.ask-user-group{display:flex;flex-direction:column;gap:6px}.ask-user-question{font-size:12px;font-weight:600;color:var(--text-2)}.ask-user-options{display:flex;flex-wrap:wrap;gap:6px}.ask-user-option{padding:4px 10px;font-size:12px;font-family:var(--font-sans);color:var(--text-2);background:var(--canvas);border:1px solid var(--border);border-radius:var(--r-sm);cursor:pointer;transition:all var(--dur-fast) var(--ease)}.ask-user-option:not(:disabled):hover{border-color:var(--accent);color:var(--accent)}.ask-user-option.selected{background:var(--accent);border-color:var(--accent);color:#fff}.ask-user-option:disabled{opacity:.55;cursor:default}.ask-user-option.selected:disabled{opacity:.8}.ask-user-send{display:inline-flex;align-items:center;gap:5px;padding:4px 12px;font-size:12px;font-family:var(--font-sans);color:#fff;background:var(--accent);border:none;border-radius:var(--r-sm);cursor:pointer;align-self:flex-start;transition:background var(--dur-fast) var(--ease)}.ask-user-send:disabled{opacity:.4;cursor:default}.ask-user-send:not(:disabled):hover{background:var(--accent-hover)}.ask-user-submitted{font-size:11px;color:var(--text-muted);font-style:italic}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>AgentPane</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link
|
|
10
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;450;510;590;600&family=JetBrains+Mono:wght@400;500&display=swap"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<script type="module" crossorigin src="/assets/index-BUkOj2Cb.js"></script>
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-gAFaOmjN.css">
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<div id="root"></div>
|
|
18
|
+
</body>
|
|
19
|
+
</html>
|
package/daemon/dist/agentPane.js
CHANGED
|
@@ -108,17 +108,22 @@ function createPaneDelegate(pane) {
|
|
|
108
108
|
pane.routing(req, res);
|
|
109
109
|
};
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
* Single upgrade router for the daemon's HTTP server.
|
|
113
|
-
*
|
|
114
|
-
* `/ws` goes to the embedded agent-pane app (when present); everything else —
|
|
115
|
-
* e.g. the platform frontend's auth WebSocket — goes to the daemon wss.
|
|
116
|
-
*/
|
|
117
|
-
function createUpgradeRouter(pane, daemonWss) {
|
|
111
|
+
function createUpgradeRouter(pane, daemonWss, guard) {
|
|
118
112
|
return (req, socket, head) => {
|
|
119
113
|
const url = req.url || '';
|
|
120
|
-
|
|
114
|
+
const guardedUpgrade = () => {
|
|
121
115
|
pane.handleWsUpgrade(req, socket, head);
|
|
116
|
+
};
|
|
117
|
+
const reject = () => {
|
|
118
|
+
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
119
|
+
socket.destroy();
|
|
120
|
+
};
|
|
121
|
+
if (pane && url.startsWith('/ws')) {
|
|
122
|
+
if (!guard) {
|
|
123
|
+
guardedUpgrade();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
void guard(req).then((allowed) => (allowed ? guardedUpgrade() : reject()));
|
|
122
127
|
return;
|
|
123
128
|
}
|
|
124
129
|
daemonWss.handleUpgrade(req, socket, head, (ws) => {
|
|
@@ -131,6 +131,15 @@ class AuthManager {
|
|
|
131
131
|
throw new Error('Could not save access token.');
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
|
+
/** Raw device JWT for outbound calls to the account server. */
|
|
135
|
+
async readToken() {
|
|
136
|
+
try {
|
|
137
|
+
return await fs_extra_1.default.readFile(TOKEN_PATH, 'utf-8');
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
134
143
|
async logout() {
|
|
135
144
|
try {
|
|
136
145
|
await fs_extra_1.default.remove(TOKEN_PATH);
|
package/daemon/dist/index.js
CHANGED
|
@@ -9,11 +9,15 @@ const http_1 = __importDefault(require("http"));
|
|
|
9
9
|
const ws_1 = require("ws");
|
|
10
10
|
const cors_1 = __importDefault(require("cors"));
|
|
11
11
|
const serviceManager_1 = require("./serviceManager");
|
|
12
|
+
const corsOrigin_1 = require("./middleware/corsOrigin");
|
|
12
13
|
const services_1 = __importDefault(require("./api/services"));
|
|
13
14
|
const auth_1 = __importDefault(require("./api/auth"));
|
|
14
15
|
const registry_1 = __importDefault(require("./api/registry"));
|
|
15
16
|
const mcp_gateway_1 = require("./mcp-gateway");
|
|
16
17
|
const agentPane_1 = require("./agentPane");
|
|
18
|
+
const routes_1 = require("./remote/routes");
|
|
19
|
+
const checkRemoteAuth_1 = require("./middleware/checkRemoteAuth");
|
|
20
|
+
const staticServing_1 = require("./staticServing");
|
|
17
21
|
const PORT = 9998;
|
|
18
22
|
async function main() {
|
|
19
23
|
await serviceManager_1.serviceManager.connect();
|
|
@@ -27,15 +31,24 @@ async function main() {
|
|
|
27
31
|
const server = http_1.default.createServer(app);
|
|
28
32
|
// Daemon wss in noServer mode — the shared upgrade router dispatches below.
|
|
29
33
|
const wss = new ws_1.WebSocketServer({ noServer: true });
|
|
34
|
+
// Spec: allowlist only — local tooling (vite, daemon-desktop) and
|
|
35
|
+
// *.repoburg.com tunnel hostnames. The previous origin:'*' with
|
|
36
|
+
// credentials:true was an open API surface.
|
|
30
37
|
app.use((0, cors_1.default)({
|
|
31
|
-
origin:
|
|
38
|
+
origin: corsOrigin_1.corsOrigin,
|
|
32
39
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
33
40
|
credentials: true,
|
|
34
41
|
}));
|
|
42
|
+
// CF Access guard for the embedded pane: when the machine has a tunnel and
|
|
43
|
+
// Access is configured, /api/* traffic must carry a valid Access JWT.
|
|
44
|
+
app.use('/api', checkRemoteAuth_1.checkRemoteAuth);
|
|
35
45
|
// Embedded agent-pane API. MUST precede express.json() — Fastify parses its
|
|
36
46
|
// own request bodies; the stream can only be consumed once.
|
|
37
47
|
app.all('/api/*', (0, agentPane_1.createPaneDelegate)(pane));
|
|
38
48
|
app.use(express_1.default.json());
|
|
49
|
+
// Tunnel lifecycle API — local-only; the CF ingress edge blocks /remote/*
|
|
50
|
+
// for tunnel traffic (see account-server CloudflareService.setIngress).
|
|
51
|
+
app.use('/remote', (0, routes_1.createRemoteRouter)());
|
|
39
52
|
// API Routers
|
|
40
53
|
app.use('/services', services_1.default);
|
|
41
54
|
app.use('/auth', (0, auth_1.default)(wss));
|
|
@@ -57,15 +70,29 @@ async function main() {
|
|
|
57
70
|
app.get('/health', (req, res) => {
|
|
58
71
|
res.status(200).json({ status: 'ok' });
|
|
59
72
|
});
|
|
73
|
+
// Pane frontend SPA — mounted LAST so API routes (including /health) are
|
|
74
|
+
// never shadowed. A machine's own origin serves the pane and the API on
|
|
75
|
+
// one origin.
|
|
76
|
+
const paneFrontendRouter = (0, staticServing_1.createPaneFrontendRouter)((0, staticServing_1.resolvePaneFrontendDist)(__dirname));
|
|
77
|
+
if (paneFrontendRouter) {
|
|
78
|
+
app.use(paneFrontendRouter);
|
|
79
|
+
console.log(`[pane] serving frontend from ${(0, staticServing_1.resolvePaneFrontendDist)(__dirname)}`);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
console.warn(`[pane] frontend dist not found at ${(0, staticServing_1.resolvePaneFrontendDist)(__dirname)} — SPA not served`);
|
|
83
|
+
}
|
|
60
84
|
wss.on('connection', (ws) => {
|
|
61
85
|
console.log('Client connected to daemon WebSocket');
|
|
62
86
|
ws.on('close', () => console.log('Client disconnected from daemon WebSocket'));
|
|
63
87
|
ws.on('error', (error) => console.error('WebSocket error:', error));
|
|
64
88
|
});
|
|
65
|
-
// Single upgrade router: '/ws' -> embedded agent-pane
|
|
66
|
-
|
|
67
|
-
server.
|
|
68
|
-
|
|
89
|
+
// Single upgrade router: '/ws' -> embedded agent-pane (guarded by CF Access
|
|
90
|
+
// when configured), everything else -> daemon wss.
|
|
91
|
+
server.on('upgrade', (0, agentPane_1.createUpgradeRouter)(pane, wss, checkRemoteAuth_1.checkRemoteWsAuth));
|
|
92
|
+
// Spec: loopback binding only — remote access is exclusively via the
|
|
93
|
+
// cloudflared tunnel; the daemon must never listen on public interfaces.
|
|
94
|
+
server.listen(PORT, '127.0.0.1', () => {
|
|
95
|
+
console.log(`Repoburg Platform Daemon listening on http://127.0.0.1:${PORT}`);
|
|
69
96
|
console.log(`MCP Gateway available at http://localhost:${PORT}/mcp`);
|
|
70
97
|
});
|
|
71
98
|
// Clean up MCP gateway sessions and the embedded agent-pane on shutdown.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkRemoteAuth = checkRemoteAuth;
|
|
4
|
+
exports.checkRemoteWsAuth = checkRemoteWsAuth;
|
|
5
|
+
const tunnel_state_1 = require("../remote/tunnel-state");
|
|
6
|
+
const access_jwt_1 = require("../remote/access-jwt");
|
|
7
|
+
/**
|
|
8
|
+
* Guard for daemon endpoints that must not be reachable by anonymous tunnel
|
|
9
|
+
* traffic once CF Access is configured.
|
|
10
|
+
*
|
|
11
|
+
* Only requests that ARRIVED THROUGH THE TUNNEL (Host = the machine's
|
|
12
|
+
* <mahlas>.repoburg.com hostname) need the Access JWT. Requests with a
|
|
13
|
+
* local Host header — the local pane at localhost:9998 and the public
|
|
14
|
+
* pane host agent.repoburg.com calling into the visitor's own daemon —
|
|
15
|
+
* pass through: the loopback binding is the boundary there, and remote
|
|
16
|
+
* clients cannot forge the Host through the CF edge (routing is by
|
|
17
|
+
* hostname/SNI).
|
|
18
|
+
*/
|
|
19
|
+
function isLocalHostHeader(host) {
|
|
20
|
+
if (!host)
|
|
21
|
+
return true; // no Host header = non-HTTP client tooling
|
|
22
|
+
return host.startsWith('localhost') || host.startsWith('127.0.0.1') || host.startsWith('0.0.0.0');
|
|
23
|
+
}
|
|
24
|
+
async function checkRemoteAuth(req, res, next) {
|
|
25
|
+
const state = (0, tunnel_state_1.loadTunnelState)();
|
|
26
|
+
const config = state?.accessTeamDomain && state.accessAud
|
|
27
|
+
? { teamDomain: state.accessTeamDomain, aud: state.accessAud }
|
|
28
|
+
: null;
|
|
29
|
+
if (!config || !state || isLocalHostHeader(req.headers.host)) {
|
|
30
|
+
// No Access configured, or local usage — loopback binding is the boundary.
|
|
31
|
+
next();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Fail closed: Access is configured but the owner email was never pinned
|
|
35
|
+
// (state from before email pinging). Re-provision the machine.
|
|
36
|
+
if (!state.ownerEmail) {
|
|
37
|
+
res.status(401).json({ error: 'Tunnel owner email is missing — re-provision this machine.' });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const assertion = req.headers['cf-access-jwt-assertion'];
|
|
41
|
+
if (typeof assertion !== 'string') {
|
|
42
|
+
res.status(401).json({ error: 'Cloudflare Access authentication required.' });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
await (0, access_jwt_1.verifyAccessJwt)(assertion, config, { ownerEmail: state.ownerEmail });
|
|
47
|
+
next();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
res.status(401).json({ error: 'Invalid or expired Cloudflare Access token.' });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Same guard for WebSocket upgrades: false = reject and destroy the socket. */
|
|
54
|
+
async function checkRemoteWsAuth(req) {
|
|
55
|
+
const state = (0, tunnel_state_1.loadTunnelState)();
|
|
56
|
+
if (!state?.accessTeamDomain || !state.accessAud)
|
|
57
|
+
return true; // no Access configured
|
|
58
|
+
if (isLocalHostHeader(req.headers.host))
|
|
59
|
+
return true; // local usage
|
|
60
|
+
if (!state.ownerEmail)
|
|
61
|
+
return false; // fail closed: no pinned owner email
|
|
62
|
+
const assertion = req.headers['cf-access-jwt-assertion'];
|
|
63
|
+
if (typeof assertion !== 'string')
|
|
64
|
+
return false;
|
|
65
|
+
try {
|
|
66
|
+
await (0, access_jwt_1.verifyAccessJwt)(assertion, { teamDomain: state.accessTeamDomain, aud: state.accessAud }, { ownerEmail: state.ownerEmail });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const checkRemoteAuth_1 = require("./checkRemoteAuth");
|
|
4
|
+
const tunnel_state_1 = require("../remote/tunnel-state");
|
|
5
|
+
const access_jwt_1 = require("../remote/access-jwt");
|
|
6
|
+
jest.mock('../remote/tunnel-state');
|
|
7
|
+
jest.mock('../remote/access-jwt');
|
|
8
|
+
const mockedLoad = tunnel_state_1.loadTunnelState;
|
|
9
|
+
const mockedVerify = access_jwt_1.verifyAccessJwt;
|
|
10
|
+
/** Requests THROUGH THE TUNNEL carry the machine hostname as Host. */
|
|
11
|
+
const TUNNEL_HOST = 'zztest.repoburg.com:443';
|
|
12
|
+
/** Fully provisioned state (Access + pinned owner email). */
|
|
13
|
+
const PINNED_STATE = {
|
|
14
|
+
accessTeamDomain: 'team.cloudflareaccess.com',
|
|
15
|
+
accessAud: 'aud-tag',
|
|
16
|
+
ownerEmail: 'owner@repoburg.com',
|
|
17
|
+
};
|
|
18
|
+
const makeRes = () => {
|
|
19
|
+
const res = { status: jest.fn(), json: jest.fn() };
|
|
20
|
+
res.status.mockReturnValue(res);
|
|
21
|
+
return res;
|
|
22
|
+
};
|
|
23
|
+
const makeReq = (headers) => ({ headers });
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mockedLoad.mockReset().mockReturnValue(null);
|
|
26
|
+
mockedVerify.mockReset().mockResolvedValue(undefined);
|
|
27
|
+
});
|
|
28
|
+
describe('checkRemoteAuth (HTTP)', () => {
|
|
29
|
+
it('passes through when no tunnel state exists (local usage)', async () => {
|
|
30
|
+
const next = jest.fn();
|
|
31
|
+
await (0, checkRemoteAuth_1.checkRemoteAuth)(makeReq({ host: 'localhost:9998' }), makeRes(), next);
|
|
32
|
+
expect(next).toHaveBeenCalled();
|
|
33
|
+
});
|
|
34
|
+
it('passes through when Access is not configured yet', async () => {
|
|
35
|
+
mockedLoad.mockReturnValue({ accessTeamDomain: undefined, accessAud: undefined });
|
|
36
|
+
const next = jest.fn();
|
|
37
|
+
await (0, checkRemoteAuth_1.checkRemoteAuth)(makeReq({ host: TUNNEL_HOST }), makeRes(), next);
|
|
38
|
+
expect(next).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
it('passes local requests even when Access is configured (local pane + agent.repoburg.com FE)', async () => {
|
|
41
|
+
mockedLoad.mockReturnValue(PINNED_STATE);
|
|
42
|
+
for (const host of ['localhost:9998', '127.0.0.1:9998', '0.0.0.0:9998']) {
|
|
43
|
+
const next = jest.fn();
|
|
44
|
+
await (0, checkRemoteAuth_1.checkRemoteAuth)(makeReq({ host }), makeRes(), next);
|
|
45
|
+
expect(next).toHaveBeenCalledWith();
|
|
46
|
+
}
|
|
47
|
+
expect(mockedVerify).not.toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
it('requires an Access assertion for tunnel-host requests when configured', async () => {
|
|
50
|
+
mockedLoad.mockReturnValue(PINNED_STATE);
|
|
51
|
+
const res = makeRes();
|
|
52
|
+
const next = jest.fn();
|
|
53
|
+
await (0, checkRemoteAuth_1.checkRemoteAuth)(makeReq({ host: TUNNEL_HOST }), res, next);
|
|
54
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
55
|
+
expect(next).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
it('FAILS CLOSED when Access is configured but no owner email is pinned (pre-pinning state)', async () => {
|
|
58
|
+
mockedLoad.mockReturnValue({ accessTeamDomain: 'team.cloudflareaccess.com', accessAud: 'aud-tag' });
|
|
59
|
+
const res = makeRes();
|
|
60
|
+
const next = jest.fn();
|
|
61
|
+
await (0, checkRemoteAuth_1.checkRemoteAuth)(makeReq({ host: TUNNEL_HOST, 'cf-access-jwt-assertion': 'jwt' }), res, next);
|
|
62
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
63
|
+
expect(mockedVerify).not.toHaveBeenCalled();
|
|
64
|
+
expect(next).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
it('accepts a valid Access JWT and pins the owner email', async () => {
|
|
67
|
+
mockedLoad.mockReturnValue(PINNED_STATE);
|
|
68
|
+
const next = jest.fn();
|
|
69
|
+
await (0, checkRemoteAuth_1.checkRemoteAuth)(makeReq({ host: TUNNEL_HOST, 'cf-access-jwt-assertion': 'jwt' }), makeRes(), next);
|
|
70
|
+
expect(mockedVerify).toHaveBeenCalledWith('jwt', { teamDomain: 'team.cloudflareaccess.com', aud: 'aud-tag' }, { ownerEmail: 'owner@repoburg.com' });
|
|
71
|
+
expect(next).toHaveBeenCalled();
|
|
72
|
+
});
|
|
73
|
+
it('rejects a valid-signature JWT for the WRONG email with 401 (email pinning)', async () => {
|
|
74
|
+
mockedLoad.mockReturnValue(PINNED_STATE);
|
|
75
|
+
// Simulates a policy edited by a compromised account-server: CF signed
|
|
76
|
+
// the token, but common_name is the attacker's email.
|
|
77
|
+
mockedVerify.mockRejectedValue(new Error('Access token email does not match the machine owner'));
|
|
78
|
+
const res = makeRes();
|
|
79
|
+
const next = jest.fn();
|
|
80
|
+
await (0, checkRemoteAuth_1.checkRemoteAuth)(makeReq({ host: TUNNEL_HOST, 'cf-access-jwt-assertion': 'jwt' }), res, next);
|
|
81
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
82
|
+
expect(next).not.toHaveBeenCalled();
|
|
83
|
+
});
|
|
84
|
+
it('rejects an invalid Access JWT with 401', async () => {
|
|
85
|
+
mockedLoad.mockReturnValue(PINNED_STATE);
|
|
86
|
+
mockedVerify.mockRejectedValue(new Error('bad token'));
|
|
87
|
+
const res = makeRes();
|
|
88
|
+
const next = jest.fn();
|
|
89
|
+
await (0, checkRemoteAuth_1.checkRemoteAuth)(makeReq({ host: TUNNEL_HOST, 'cf-access-jwt-assertion': 'jwt' }), res, next);
|
|
90
|
+
expect(res.status).toHaveBeenCalledWith(401);
|
|
91
|
+
expect(next).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('checkRemoteWsAuth (WebSocket upgrade)', () => {
|
|
95
|
+
it('allows everything when Access is not configured', async () => {
|
|
96
|
+
mockedLoad.mockReturnValue(null);
|
|
97
|
+
await expect((0, checkRemoteAuth_1.checkRemoteWsAuth)(makeReq({ host: 'localhost:9998' }))).resolves.toBe(true);
|
|
98
|
+
});
|
|
99
|
+
it('allows local upgrades even when configured', async () => {
|
|
100
|
+
mockedLoad.mockReturnValue(PINNED_STATE);
|
|
101
|
+
await expect((0, checkRemoteAuth_1.checkRemoteWsAuth)(makeReq({ host: 'localhost:9998' }))).resolves.toBe(true);
|
|
102
|
+
expect(mockedVerify).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
it('denies tunnel-host upgrades without an assertion when configured', async () => {
|
|
105
|
+
mockedLoad.mockReturnValue(PINNED_STATE);
|
|
106
|
+
await expect((0, checkRemoteAuth_1.checkRemoteWsAuth)(makeReq({ host: TUNNEL_HOST }))).resolves.toBe(false);
|
|
107
|
+
});
|
|
108
|
+
it('fails closed when configured without a pinned owner email', async () => {
|
|
109
|
+
mockedLoad.mockReturnValue({ accessTeamDomain: 'team.cloudflareaccess.com', accessAud: 'aud' });
|
|
110
|
+
await expect((0, checkRemoteAuth_1.checkRemoteWsAuth)(makeReq({ host: TUNNEL_HOST }))).resolves.toBe(false);
|
|
111
|
+
});
|
|
112
|
+
it('allows tunnel-host upgrades with a valid assertion and pins the owner email', async () => {
|
|
113
|
+
mockedLoad.mockReturnValue(PINNED_STATE);
|
|
114
|
+
await expect((0, checkRemoteAuth_1.checkRemoteWsAuth)(makeReq({ host: TUNNEL_HOST, 'cf-access-jwt-assertion': 'jwt' }))).resolves.toBe(true);
|
|
115
|
+
expect(mockedVerify).toHaveBeenCalledWith('jwt', { teamDomain: 'team.cloudflareaccess.com', aud: 'aud-tag' }, { ownerEmail: 'owner@repoburg.com' });
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.corsOrigin = corsOrigin;
|
|
4
|
+
exports.setExtraOriginsForTest = setExtraOriginsForTest;
|
|
5
|
+
/**
|
|
6
|
+
* CORS origin allowlist for the daemon.
|
|
7
|
+
*
|
|
8
|
+
* Spec: the daemon serves the embedded agent-pane to local tooling
|
|
9
|
+
* (vite dev servers, daemon-desktop) and, once a tunnel is provisioned, to
|
|
10
|
+
* `*.repoburg.com` machine hostnames. Everything else is rejected — the
|
|
11
|
+
* previous `origin: '*'` with `credentials: true` is an open API surface.
|
|
12
|
+
*
|
|
13
|
+
* Allowed:
|
|
14
|
+
* - no Origin header (same-origin / non-browser clients such as curl)
|
|
15
|
+
* - http://localhost:<any> and http://127.0.0.1:<any>
|
|
16
|
+
* - https://<anything>.repoburg.com (tunnel machine hostnames)
|
|
17
|
+
* - extra origins from REPOBURG_ALLOWED_ORIGINS (comma-separated)
|
|
18
|
+
*/
|
|
19
|
+
const REPORG_HOSTNAME_SUFFIX = '.repoburg.com';
|
|
20
|
+
const extraOrigins = (process.env.REPOBURG_ALLOWED_ORIGINS ?? '')
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((value) => value.trim())
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
function isLocalOrigin(origin) {
|
|
25
|
+
return (origin.protocol === 'http:' &&
|
|
26
|
+
(origin.hostname === 'localhost' || origin.hostname === '127.0.0.1'));
|
|
27
|
+
}
|
|
28
|
+
function isTunnelOrigin(origin) {
|
|
29
|
+
return origin.protocol === 'https:' && origin.hostname.endsWith(REPORG_HOSTNAME_SUFFIX);
|
|
30
|
+
}
|
|
31
|
+
/** CORS origin callback. `false` = do not emit CORS headers for this origin. */
|
|
32
|
+
function corsOrigin(origin, callback) {
|
|
33
|
+
// Same-origin requests and non-browser clients send no Origin header.
|
|
34
|
+
if (origin === undefined) {
|
|
35
|
+
callback(null, true);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = new URL(origin);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
callback(null, false);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const allowed = isLocalOrigin(parsed) || isTunnelOrigin(parsed) || extraOrigins.includes(origin);
|
|
47
|
+
callback(null, allowed);
|
|
48
|
+
}
|
|
49
|
+
/** Exposed for tests. */
|
|
50
|
+
function setExtraOriginsForTest(origins) {
|
|
51
|
+
extraOrigins.length = 0;
|
|
52
|
+
extraOrigins.push(...origins);
|
|
53
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const corsOrigin_1 = require("./corsOrigin");
|
|
4
|
+
function evaluate(origin) {
|
|
5
|
+
let result = false;
|
|
6
|
+
(0, corsOrigin_1.corsOrigin)(origin, (err, allow) => {
|
|
7
|
+
if (err)
|
|
8
|
+
throw err;
|
|
9
|
+
result = !!allow;
|
|
10
|
+
});
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
describe('corsOrigin', () => {
|
|
14
|
+
afterEach(() => (0, corsOrigin_1.setExtraOriginsForTest)([]));
|
|
15
|
+
it('allows requests without an Origin header (same-origin / curl)', () => {
|
|
16
|
+
expect(evaluate(undefined)).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
it('allows localhost and 127.0.0.1 on any port', () => {
|
|
19
|
+
expect(evaluate('http://localhost:3000')).toBe(true);
|
|
20
|
+
expect(evaluate('http://localhost:9998')).toBe(true);
|
|
21
|
+
expect(evaluate('http://127.0.0.1:5173')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it('allows tunnel machine hostnames under repoburg.com (https only)', () => {
|
|
24
|
+
expect(evaluate('https://workstation.repoburg.com')).toBe(true);
|
|
25
|
+
expect(evaluate('https://a-b.repoburg.com')).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
it('rejects unknown origins', () => {
|
|
28
|
+
expect(evaluate('https://evil.example.com')).toBe(false);
|
|
29
|
+
expect(evaluate('http://evil.com')).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
it('rejects repoburg.com hostnames over http (tunnels are https)', () => {
|
|
32
|
+
expect(evaluate('http://workstation.repoburg.com')).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
it('rejects lookalike suffixes', () => {
|
|
35
|
+
expect(evaluate('https://workstation.repoburg.com.evil.com')).toBe(false);
|
|
36
|
+
expect(evaluate('https://evilrepoburg.com')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
it('rejects malformed origins', () => {
|
|
39
|
+
expect(evaluate('not a url')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
it('allows extra origins configured via env list', () => {
|
|
42
|
+
(0, corsOrigin_1.setExtraOriginsForTest)(['https://staging.example.com']);
|
|
43
|
+
expect(evaluate('https://staging.example.com')).toBe(true);
|
|
44
|
+
expect(evaluate('https://other.example.com')).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.teamJwks = teamJwks;
|
|
37
|
+
exports.verifyAccessJwt = verifyAccessJwt;
|
|
38
|
+
/**
|
|
39
|
+
* CF Access JWT verification for requests arriving through the tunnel.
|
|
40
|
+
*
|
|
41
|
+
* Spec: when the machine has a tunnel AND Cloudflare Access is configured
|
|
42
|
+
* (team domain + application AUD stored in remote.json), every /api/* and
|
|
43
|
+
* /ws request must carry a valid `Cf-Access-Jwt-Assertion` header signed by
|
|
44
|
+
* the team's Cloudflare Zero Trust org. Without Access configured, the
|
|
45
|
+
* loopback binding remains the security boundary (tunnel traffic is
|
|
46
|
+
* Edge-protected by the user's Access policy once set up).
|
|
47
|
+
*/
|
|
48
|
+
const jose = __importStar(require("jose"));
|
|
49
|
+
/** Build (and cache) the JWKS for a team's public Access signing keys. */
|
|
50
|
+
function teamJwks(teamDomain) {
|
|
51
|
+
const url = new URL(`https://${teamDomain}/cdn-cgi/access/certs`);
|
|
52
|
+
return jose.createRemoteJWKSet(url);
|
|
53
|
+
}
|
|
54
|
+
/** Verify a Cloudflare Access JWT against the team's keys. */
|
|
55
|
+
async function verifyAccessJwt(assertion, config, options = {}) {
|
|
56
|
+
const jwks = options.jwks ?? teamJwks(config.teamDomain);
|
|
57
|
+
const { payload } = await jose.jwtVerify(assertion, jwks, {
|
|
58
|
+
issuer: `https://${config.teamDomain}`,
|
|
59
|
+
audience: config.aud,
|
|
60
|
+
});
|
|
61
|
+
// CF Access JWTs carry the identity email in `email`; older docs/flows used
|
|
62
|
+
// `common_name`. Accept either so pinning matches real edge-issued tokens.
|
|
63
|
+
const identity = payload.common_name ?? payload.email;
|
|
64
|
+
if (options.ownerEmail && identity !== options.ownerEmail) {
|
|
65
|
+
throw new Error(`Access token email does not match the machine owner (${identity} !== ${options.ownerEmail}).`);
|
|
66
|
+
}
|
|
67
|
+
}
|