@prismshadow/penguin-server 0.2.11 → 0.2.13
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/README.md +1 -1
- package/dist/api/types.d.ts +1014 -12
- package/dist/api/types.js +7 -0
- package/dist/auth-token.js +6 -4
- package/dist/auth-token.js.map +1 -1
- package/dist/{chunk-Q65VPAQS.js → chunk-3I2XOPJB.js} +102 -55
- package/dist/chunk-3I2XOPJB.js.map +1 -0
- package/dist/{chunk-ADZJYDNC.js → chunk-A3QANMU5.js} +293 -3
- package/dist/chunk-A3QANMU5.js.map +1 -0
- package/dist/chunk-B5IP37ZQ.js +56 -0
- package/dist/chunk-L2H3K4DT.js +7 -0
- package/dist/chunk-L2H3K4DT.js.map +1 -0
- package/dist/chunk-LPPVGHYD.js +83 -0
- package/dist/chunk-LPPVGHYD.js.map +1 -0
- package/dist/{chunk-TTGGJDNA.js → chunk-S3BI6R7I.js} +2 -2
- package/dist/{chunk-HOS3W3UP.js → chunk-TOOTEEM4.js} +50 -8
- package/dist/chunk-TOOTEEM4.js.map +1 -0
- package/dist/hmr/manifest.d.ts +1 -77
- package/dist/hmr/manifest.js +6 -1
- package/dist/index.js +50757 -20174
- package/dist/index.js.map +1 -1
- package/dist/initial-password.js +1 -0
- package/dist/lock.js +1 -0
- package/dist/machine-status.js +2 -1
- package/dist/machine-status.js.map +1 -1
- package/dist/plugin/index.d.ts +2595 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/reset-admin-password.js +7 -5
- package/dist/reset-admin-password.js.map +1 -1
- package/dist/secret-file.js +2 -0
- package/dist/secret-file.js.map +1 -1
- package/dist/version-report.js +3 -2
- package/package.json +11 -9
- package/web-dist/assets/{addon-clipboard-1cARCaK9.js → addon-clipboard-DrZY90Ks.js} +1 -1
- package/web-dist/assets/{addon-fit-BWdzl8Er.js → addon-fit-CtEAdAax.js} +1 -1
- package/web-dist/assets/{addon-web-links-DjXuvn4Z.js → addon-web-links-DJL13cYY.js} +1 -1
- package/web-dist/assets/c-BG-tZi7o.js +1 -0
- package/web-dist/assets/cpp-CaeG3uDQ.js +1 -0
- package/web-dist/assets/css-Br67jR8j.js +1 -0
- package/web-dist/assets/diff-jW5kmiru.js +1 -0
- package/web-dist/assets/github-dark-BQV5ugrc.js +1 -0
- package/web-dist/assets/github-light-Rcb2q3L5.js +1 -0
- package/web-dist/assets/go-B7VTfpnm.js +1 -0
- package/web-dist/assets/highlighter-CUb4XKtQ.js +2 -0
- package/web-dist/assets/{highlighter-Cu43iYr8.js → highlighter-core-CrBdruEx.js} +2 -2
- package/web-dist/assets/highlighter.worker-CL6f9Fzw.js +152 -0
- package/web-dist/assets/html-BW_6VhB7.js +1 -0
- package/web-dist/assets/index-CmfumtXy.js +570 -0
- package/web-dist/assets/index-Ywc2uM5u.css +1 -0
- package/web-dist/assets/ini-Dix3pTmg.js +1 -0
- package/web-dist/assets/java-BTDmaRNO.js +1 -0
- package/web-dist/assets/javascript-WyGyM1wV.js +1 -0
- package/web-dist/assets/json-B8JryHcM.js +1 -0
- package/web-dist/assets/jsx-8cLv2Six.js +1 -0
- package/web-dist/assets/log-CcaF_xNv.js +1 -0
- package/web-dist/assets/markdown-DX-TNHnc.js +1 -0
- package/web-dist/assets/php-CNeTOyEV.js +1 -0
- package/web-dist/assets/python-B6Ihcp-E.js +1 -0
- package/web-dist/assets/ruby-CYiNE6ps.js +1 -0
- package/web-dist/assets/rust-CxuBE0J8.js +1 -0
- package/web-dist/assets/shellscript-DgJyDXW6.js +1 -0
- package/web-dist/assets/sql-Dq2MiB0T.js +1 -0
- package/web-dist/assets/toml-Ms459DBw.js +1 -0
- package/web-dist/assets/tsx-D6116Ycy.js +1 -0
- package/web-dist/assets/typescript-BqvgUolh.js +1 -0
- package/web-dist/assets/xml-BkAJuBCc.js +1 -0
- package/web-dist/assets/{xterm-DKcY6q-I.js → xterm-B2oS8Mg8.js} +1 -1
- package/web-dist/assets/yaml-Bopit2L3.js +1 -0
- package/web-dist/index.html +2 -2
- package/dist/chunk-4SXGU7RO.js +0 -50
- package/dist/chunk-4SXGU7RO.js.map +0 -1
- package/dist/chunk-ADZJYDNC.js.map +0 -1
- package/dist/chunk-HOS3W3UP.js.map +0 -1
- package/dist/chunk-Q65VPAQS.js.map +0 -1
- package/dist/extension/index.d.ts +0 -212
- package/web-dist/assets/index-Bm9n7ZV-.js +0 -457
- package/web-dist/assets/index-DR4PrRzS.css +0 -1
- /package/dist/{extension/index.js.map → chunk-B5IP37ZQ.js.map} +0 -0
- /package/dist/{chunk-TTGGJDNA.js.map → chunk-S3BI6R7I.js.map} +0 -0
- /package/dist/{extension → plugin}/index.js +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{g as Ce}from"./index-
|
|
1
|
+
import{g as Ce}from"./index-CmfumtXy.js";function be(se,le){for(var te=0;te<le.length;te++){const Y=le[te];if(typeof Y!="string"&&!Array.isArray(Y)){for(const G in Y)if(G!=="default"&&!(G in se)){const ie=Object.getOwnPropertyDescriptor(Y,G);ie&&Object.defineProperty(se,G,ie.get?ie:{enumerable:!0,get:()=>Y[G]})}}}return Object.freeze(Object.defineProperty(se,Symbol.toStringTag,{value:"Module"}))}var ve={exports:{}},pe;function we(){return pe||(pe=1,(function(se,le){(function(te,Y){se.exports=Y()})(globalThis,(()=>(()=>{var te={4567:function(O,r,o){var l=this&&this.__decorate||function(e,t,a,u){var _,p=arguments.length,c=p<3?t:u===null?u=Object.getOwnPropertyDescriptor(t,a):u;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")c=Reflect.decorate(e,t,a,u);else for(var m=e.length-1;m>=0;m--)(_=e[m])&&(c=(p<3?_(c):p>3?_(t,a,c):_(t,a))||c);return p>3&&c&&Object.defineProperty(t,a,c),c},f=this&&this.__param||function(e,t){return function(a,u){t(a,u,e)}};Object.defineProperty(r,"__esModule",{value:!0}),r.AccessibilityManager=void 0;const n=o(9042),d=o(9924),v=o(844),g=o(4725),h=o(2585),i=o(3656);let s=r.AccessibilityManager=class extends v.Disposable{constructor(e,t,a,u){super(),this._terminal=e,this._coreBrowserService=a,this._renderService=u,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let _=0;_<this._terminal.rows;_++)this._rowElements[_]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[_]);if(this._topBoundaryFocusListener=_=>this._handleBoundaryFocus(_,0),this._bottomBoundaryFocusListener=_=>this._handleBoundaryFocus(_,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new d.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize((_=>this._handleResize(_.rows)))),this.register(this._terminal.onRender((_=>this._refreshRows(_.start,_.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((_=>this._handleChar(_)))),this.register(this._terminal.onLineFeed((()=>this._handleChar(`
|
|
2
2
|
`)))),this.register(this._terminal.onA11yTab((_=>this._handleTab(_)))),this.register(this._terminal.onKey((_=>this._handleKey(_.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this.register((0,i.addDisposableDomListener)(document,"selectionchange",(()=>this._handleSelectionChange()))),this.register(this._coreBrowserService.onDprChange((()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,v.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(e){for(let t=0;t<e;t++)this._handleChar(" ")}_handleChar(e){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==e&&(this._charsToAnnounce+=e):this._charsToAnnounce+=e,e===`
|
|
3
3
|
`&&(this._liveRegionLineCount++,this._liveRegionLineCount===21&&(this._liveRegion.textContent+=n.tooMuchOutput)))}_clearLiveRegion(){this._liveRegion.textContent="",this._liveRegionLineCount=0}_handleKey(e){this._clearLiveRegion(),new RegExp("\\p{Control}","u").test(e)||this._charsToConsume.push(e)}_refreshRows(e,t){this._liveRegionDebouncer.refresh(e,t,this._terminal.rows)}_renderRows(e,t){const a=this._terminal.buffer,u=a.lines.length.toString();for(let _=e;_<=t;_++){const p=a.lines.get(a.ydisp+_),c=[],m=p?.translateToString(!0,void 0,void 0,c)||"",E=(a.ydisp+_+1).toString(),k=this._rowElements[_];k&&(m.length===0?(k.innerText=" ",this._rowColumns.set(k,[0,1])):(k.textContent=m,this._rowColumns.set(k,c)),k.setAttribute("aria-posinset",E),k.setAttribute("aria-setsize",u))}this._announceCharacters()}_announceCharacters(){this._charsToAnnounce.length!==0&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce="")}_handleBoundaryFocus(e,t){const a=e.target,u=this._rowElements[t===0?1:this._rowElements.length-2];if(a.getAttribute("aria-posinset")===(t===0?"1":`${this._terminal.buffer.lines.length}`)||e.relatedTarget!==u)return;let _,p;if(t===0?(_=a,p=this._rowElements.pop(),this._rowContainer.removeChild(p)):(_=this._rowElements.shift(),p=a,this._rowContainer.removeChild(_)),_.removeEventListener("focus",this._topBoundaryFocusListener),p.removeEventListener("focus",this._bottomBoundaryFocusListener),t===0){const c=this._createAccessibilityTreeNode();this._rowElements.unshift(c),this._rowContainer.insertAdjacentElement("afterbegin",c)}else{const c=this._createAccessibilityTreeNode();this._rowElements.push(c),this._rowContainer.appendChild(c)}this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._terminal.scrollLines(t===0?-1:1),this._rowElements[t===0?1:this._rowElements.length-2].focus(),e.preventDefault(),e.stopImmediatePropagation()}_handleSelectionChange(){if(this._rowElements.length===0)return;const e=document.getSelection();if(!e)return;if(e.isCollapsed)return void(this._rowContainer.contains(e.anchorNode)&&this._terminal.clearSelection());if(!e.anchorNode||!e.focusNode)return void console.error("anchorNode and/or focusNode are null");let t={node:e.anchorNode,offset:e.anchorOffset},a={node:e.focusNode,offset:e.focusOffset};if((t.node.compareDocumentPosition(a.node)&Node.DOCUMENT_POSITION_PRECEDING||t.node===a.node&&t.offset>a.offset)&&([t,a]=[a,t]),t.node.compareDocumentPosition(this._rowElements[0])&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_FOLLOWING)&&(t={node:this._rowElements[0].childNodes[0],offset:0}),!this._rowContainer.contains(t.node))return;const u=this._rowElements.slice(-1)[0];if(a.node.compareDocumentPosition(u)&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_PRECEDING)&&(a={node:u,offset:u.textContent?.length??0}),!this._rowContainer.contains(a.node))return;const _=({node:m,offset:E})=>{const k=m instanceof Text?m.parentNode:m;let D=parseInt(k?.getAttribute("aria-posinset"),10)-1;if(isNaN(D))return console.warn("row is invalid. Race condition?"),null;const b=this._rowColumns.get(k);if(!b)return console.warn("columns is null. Race condition?"),null;let x=E<b.length?b[E]:b.slice(-1)[0]+1;return x>=this._terminal.cols&&(++D,x=0),{row:D,column:x}},p=_(t),c=_(a);if(p&&c){if(p.row>c.row||p.row===c.row&&p.column>=c.column)throw new Error("invalid range");this._terminal.select(p.column,p.row,(c.row-p.row)*this._terminal.cols-p.column+c.column)}}_handleResize(e){this._rowElements[this._rowElements.length-1].removeEventListener("focus",this._bottomBoundaryFocusListener);for(let t=this._rowContainer.children.length;t<this._terminal.rows;t++)this._rowElements[t]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[t]);for(;this._rowElements.length>e;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){const e=this._coreBrowserService.mainDocument.createElement("div");return e.setAttribute("role","listitem"),e.tabIndex=-1,this._refreshRowDimensions(e),e}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){this._accessibilityContainer.style.width=`${this._renderService.dimensions.css.canvas.width}px`,this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let e=0;e<this._terminal.rows;e++)this._refreshRowDimensions(this._rowElements[e])}}_refreshRowDimensions(e){e.style.height=`${this._renderService.dimensions.css.cell.height}px`}};r.AccessibilityManager=s=l([f(1,h.IInstantiationService),f(2,g.ICoreBrowserService),f(3,g.IRenderService)],s)},3614:(O,r)=>{function o(d){return d.replace(/\r?\n/g,"\r")}function l(d,v){return v?"\x1B[200~"+d+"\x1B[201~":d}function f(d,v,g,h){d=l(d=o(d),g.decPrivateModes.bracketedPasteMode&&h.rawOptions.ignoreBracketedPasteMode!==!0),g.triggerDataEvent(d,!0),v.value=""}function n(d,v,g){const h=g.getBoundingClientRect(),i=d.clientX-h.left-10,s=d.clientY-h.top-10;v.style.width="20px",v.style.height="20px",v.style.left=`${i}px`,v.style.top=`${s}px`,v.style.zIndex="1000",v.focus()}Object.defineProperty(r,"__esModule",{value:!0}),r.rightClickHandler=r.moveTextAreaUnderMouseCursor=r.paste=r.handlePasteEvent=r.copyHandler=r.bracketTextForPaste=r.prepareTextForTerminal=void 0,r.prepareTextForTerminal=o,r.bracketTextForPaste=l,r.copyHandler=function(d,v){d.clipboardData&&d.clipboardData.setData("text/plain",v.selectionText),d.preventDefault()},r.handlePasteEvent=function(d,v,g,h){d.stopPropagation(),d.clipboardData&&f(d.clipboardData.getData("text/plain"),v,g,h)},r.paste=f,r.moveTextAreaUnderMouseCursor=n,r.rightClickHandler=function(d,v,g,h,i){n(d,v,g),i&&h.rightClickSelect(d),v.value=h.selectionText,v.select()}},7239:(O,r,o)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.ColorContrastCache=void 0;const l=o(1505);r.ColorContrastCache=class{constructor(){this._color=new l.TwoKeyMap,this._css=new l.TwoKeyMap}setCss(f,n,d){this._css.set(f,n,d)}getCss(f,n){return this._css.get(f,n)}setColor(f,n,d){this._color.set(f,n,d)}getColor(f,n){return this._color.get(f,n)}clear(){this._color.clear(),this._css.clear()}}},3656:(O,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.addDisposableDomListener=void 0,r.addDisposableDomListener=function(o,l,f,n){o.addEventListener(l,f,n);let d=!1;return{dispose:()=>{d||(d=!0,o.removeEventListener(l,f,n))}}}},3551:function(O,r,o){var l=this&&this.__decorate||function(s,e,t,a){var u,_=arguments.length,p=_<3?e:a===null?a=Object.getOwnPropertyDescriptor(e,t):a;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")p=Reflect.decorate(s,e,t,a);else for(var c=s.length-1;c>=0;c--)(u=s[c])&&(p=(_<3?u(p):_>3?u(e,t,p):u(e,t))||p);return _>3&&p&&Object.defineProperty(e,t,p),p},f=this&&this.__param||function(s,e){return function(t,a){e(t,a,s)}};Object.defineProperty(r,"__esModule",{value:!0}),r.Linkifier=void 0;const n=o(3656),d=o(8460),v=o(844),g=o(2585),h=o(4725);let i=r.Linkifier=class extends v.Disposable{get currentLink(){return this._currentLink}constructor(s,e,t,a,u){super(),this._element=s,this._mouseService=e,this._renderService=t,this._bufferService=a,this._linkProviderService=u,this._linkCacheDisposables=[],this._isMouseOut=!0,this._wasResized=!1,this._activeLine=-1,this._onShowLinkUnderline=this.register(new d.EventEmitter),this.onShowLinkUnderline=this._onShowLinkUnderline.event,this._onHideLinkUnderline=this.register(new d.EventEmitter),this.onHideLinkUnderline=this._onHideLinkUnderline.event,this.register((0,v.getDisposeArrayDisposable)(this._linkCacheDisposables)),this.register((0,v.toDisposable)((()=>{this._lastMouseEvent=void 0,this._activeProviderReplies?.clear()}))),this.register(this._bufferService.onResize((()=>{this._clearCurrentLink(),this._wasResized=!0}))),this.register((0,n.addDisposableDomListener)(this._element,"mouseleave",(()=>{this._isMouseOut=!0,this._clearCurrentLink()}))),this.register((0,n.addDisposableDomListener)(this._element,"mousemove",this._handleMouseMove.bind(this))),this.register((0,n.addDisposableDomListener)(this._element,"mousedown",this._handleMouseDown.bind(this))),this.register((0,n.addDisposableDomListener)(this._element,"mouseup",this._handleMouseUp.bind(this)))}_handleMouseMove(s){this._lastMouseEvent=s;const e=this._positionFromMouseEvent(s,this._element,this._mouseService);if(!e)return;this._isMouseOut=!1;const t=s.composedPath();for(let a=0;a<t.length;a++){const u=t[a];if(u.classList.contains("xterm"))break;if(u.classList.contains("xterm-hover"))return}this._lastBufferCell&&e.x===this._lastBufferCell.x&&e.y===this._lastBufferCell.y||(this._handleHover(e),this._lastBufferCell=e)}_handleHover(s){if(this._activeLine!==s.y||this._wasResized)return this._clearCurrentLink(),this._askForLink(s,!1),void(this._wasResized=!1);this._currentLink&&this._linkAtPosition(this._currentLink.link,s)||(this._clearCurrentLink(),this._askForLink(s,!0))}_askForLink(s,e){this._activeProviderReplies&&e||(this._activeProviderReplies?.forEach((a=>{a?.forEach((u=>{u.link.dispose&&u.link.dispose()}))})),this._activeProviderReplies=new Map,this._activeLine=s.y);let t=!1;for(const[a,u]of this._linkProviderService.linkProviders.entries())e?this._activeProviderReplies?.get(a)&&(t=this._checkLinkProviderResult(a,s,t)):u.provideLinks(s.y,(_=>{if(this._isMouseOut)return;const p=_?.map((c=>({link:c})));this._activeProviderReplies?.set(a,p),t=this._checkLinkProviderResult(a,s,t),this._activeProviderReplies?.size===this._linkProviderService.linkProviders.length&&this._removeIntersectingLinks(s.y,this._activeProviderReplies)}))}_removeIntersectingLinks(s,e){const t=new Set;for(let a=0;a<e.size;a++){const u=e.get(a);if(u)for(let _=0;_<u.length;_++){const p=u[_],c=p.link.range.start.y<s?0:p.link.range.start.x,m=p.link.range.end.y>s?this._bufferService.cols:p.link.range.end.x;for(let E=c;E<=m;E++){if(t.has(E)){u.splice(_--,1);break}t.add(E)}}}}_checkLinkProviderResult(s,e,t){if(!this._activeProviderReplies)return t;const a=this._activeProviderReplies.get(s);let u=!1;for(let _=0;_<s;_++)this._activeProviderReplies.has(_)&&!this._activeProviderReplies.get(_)||(u=!0);if(!u&&a){const _=a.find((p=>this._linkAtPosition(p.link,e)));_&&(t=!0,this._handleNewLink(_))}if(this._activeProviderReplies.size===this._linkProviderService.linkProviders.length&&!t)for(let _=0;_<this._activeProviderReplies.size;_++){const p=this._activeProviderReplies.get(_)?.find((c=>this._linkAtPosition(c.link,e)));if(p){t=!0,this._handleNewLink(p);break}}return t}_handleMouseDown(){this._mouseDownLink=this._currentLink}_handleMouseUp(s){if(!this._currentLink)return;const e=this._positionFromMouseEvent(s,this._element,this._mouseService);e&&this._mouseDownLink===this._currentLink&&this._linkAtPosition(this._currentLink.link,e)&&this._currentLink.link.activate(s,this._currentLink.link.text)}_clearCurrentLink(s,e){this._currentLink&&this._lastMouseEvent&&(!s||!e||this._currentLink.link.range.start.y>=s&&this._currentLink.link.range.end.y<=e)&&(this._linkLeave(this._element,this._currentLink.link,this._lastMouseEvent),this._currentLink=void 0,(0,v.disposeArray)(this._linkCacheDisposables))}_handleNewLink(s){if(!this._lastMouseEvent)return;const e=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);e&&this._linkAtPosition(s.link,e)&&(this._currentLink=s,this._currentLink.state={decorations:{underline:s.link.decorations===void 0||s.link.decorations.underline,pointerCursor:s.link.decorations===void 0||s.link.decorations.pointerCursor},isHovered:!0},this._linkHover(this._element,s.link,this._lastMouseEvent),s.link.decorations={},Object.defineProperties(s.link.decorations,{pointerCursor:{get:()=>this._currentLink?.state?.decorations.pointerCursor,set:t=>{this._currentLink?.state&&this._currentLink.state.decorations.pointerCursor!==t&&(this._currentLink.state.decorations.pointerCursor=t,this._currentLink.state.isHovered&&this._element.classList.toggle("xterm-cursor-pointer",t))}},underline:{get:()=>this._currentLink?.state?.decorations.underline,set:t=>{this._currentLink?.state&&this._currentLink?.state?.decorations.underline!==t&&(this._currentLink.state.decorations.underline=t,this._currentLink.state.isHovered&&this._fireUnderlineEvent(s.link,t))}}}),this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange((t=>{if(!this._currentLink)return;const a=t.start===0?0:t.start+1+this._bufferService.buffer.ydisp,u=this._bufferService.buffer.ydisp+1+t.end;if(this._currentLink.link.range.start.y>=a&&this._currentLink.link.range.end.y<=u&&(this._clearCurrentLink(a,u),this._lastMouseEvent)){const _=this._positionFromMouseEvent(this._lastMouseEvent,this._element,this._mouseService);_&&this._askForLink(_,!1)}}))))}_linkHover(s,e,t){this._currentLink?.state&&(this._currentLink.state.isHovered=!0,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(e,!0),this._currentLink.state.decorations.pointerCursor&&s.classList.add("xterm-cursor-pointer")),e.hover&&e.hover(t,e.text)}_fireUnderlineEvent(s,e){const t=s.range,a=this._bufferService.buffer.ydisp,u=this._createLinkUnderlineEvent(t.start.x-1,t.start.y-a-1,t.end.x,t.end.y-a-1,void 0);(e?this._onShowLinkUnderline:this._onHideLinkUnderline).fire(u)}_linkLeave(s,e,t){this._currentLink?.state&&(this._currentLink.state.isHovered=!1,this._currentLink.state.decorations.underline&&this._fireUnderlineEvent(e,!1),this._currentLink.state.decorations.pointerCursor&&s.classList.remove("xterm-cursor-pointer")),e.leave&&e.leave(t,e.text)}_linkAtPosition(s,e){const t=s.range.start.y*this._bufferService.cols+s.range.start.x,a=s.range.end.y*this._bufferService.cols+s.range.end.x,u=e.y*this._bufferService.cols+e.x;return t<=u&&u<=a}_positionFromMouseEvent(s,e,t){const a=t.getCoords(s,e,this._bufferService.cols,this._bufferService.rows);if(a)return{x:a[0],y:a[1]+this._bufferService.buffer.ydisp}}_createLinkUnderlineEvent(s,e,t,a,u){return{x1:s,y1:e,x2:t,y2:a,cols:this._bufferService.cols,fg:u}}};r.Linkifier=i=l([f(1,h.IMouseService),f(2,h.IRenderService),f(3,g.IBufferService),f(4,h.ILinkProviderService)],i)},9042:(O,r)=>{Object.defineProperty(r,"__esModule",{value:!0}),r.tooMuchOutput=r.promptLabel=void 0,r.promptLabel="Terminal input",r.tooMuchOutput="Too much output to announce, navigate to rows manually to read"},3730:function(O,r,o){var l=this&&this.__decorate||function(h,i,s,e){var t,a=arguments.length,u=a<3?i:e===null?e=Object.getOwnPropertyDescriptor(i,s):e;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")u=Reflect.decorate(h,i,s,e);else for(var _=h.length-1;_>=0;_--)(t=h[_])&&(u=(a<3?t(u):a>3?t(i,s,u):t(i,s))||u);return a>3&&u&&Object.defineProperty(i,s,u),u},f=this&&this.__param||function(h,i){return function(s,e){i(s,e,h)}};Object.defineProperty(r,"__esModule",{value:!0}),r.OscLinkProvider=void 0;const n=o(511),d=o(2585);let v=r.OscLinkProvider=class{constructor(h,i,s){this._bufferService=h,this._optionsService=i,this._oscLinkService=s}provideLinks(h,i){const s=this._bufferService.buffer.lines.get(h-1);if(!s)return void i(void 0);const e=[],t=this._optionsService.rawOptions.linkHandler,a=new n.CellData,u=s.getTrimmedLength();let _=-1,p=-1,c=!1;for(let m=0;m<u;m++)if(p!==-1||s.hasContent(m)){if(s.loadCell(m,a),a.hasExtendedAttrs()&&a.extended.urlId){if(p===-1){p=m,_=a.extended.urlId;continue}c=a.extended.urlId!==_}else p!==-1&&(c=!0);if(c||p!==-1&&m===u-1){const E=this._oscLinkService.getLinkData(_)?.uri;if(E){const k={start:{x:p+1,y:h},end:{x:m+(c||m!==u-1?0:1),y:h}};let D=!1;if(!t?.allowNonHttpProtocols)try{const b=new URL(E);["http:","https:"].includes(b.protocol)||(D=!0)}catch{D=!0}D||e.push({text:E,range:k,activate:(b,x)=>t?t.activate(b,x,k):g(0,x),hover:(b,x)=>t?.hover?.(b,x,k),leave:(b,x)=>t?.leave?.(b,x,k)})}c=!1,a.hasExtendedAttrs()&&a.extended.urlId?(p=m,_=a.extended.urlId):(p=-1,_=-1)}}i(e)}};function g(h,i){if(confirm(`Do you want to navigate to ${i}?
|
|
4
4
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=Object.freeze(JSON.parse(`{"displayName":"YAML","fileTypes":["yaml","yml","rviz","reek","clang-format","yaml-tmlanguage","syntax","sublime-syntax"],"firstLineMatch":"^%YAML( ?1.\\\\d+)?","name":"yaml","patterns":[{"include":"#comment"},{"include":"#property"},{"include":"#directive"},{"match":"^---","name":"entity.other.document.begin.yaml"},{"match":"^\\\\.{3}","name":"entity.other.document.end.yaml"},{"include":"#node"}],"repository":{"block-collection":{"patterns":[{"include":"#block-sequence"},{"include":"#block-mapping"}]},"block-mapping":{"patterns":[{"include":"#block-pair"}]},"block-node":{"patterns":[{"include":"#prototype"},{"include":"#block-scalar"},{"include":"#block-collection"},{"include":"#flow-scalar-plain-out"},{"include":"#flow-node"}]},"block-pair":{"patterns":[{"begin":"\\\\?","beginCaptures":{"1":{"name":"punctuation.definition.key-value.begin.yaml"}},"end":"(?=\\\\?)|^ *(:)|(:)","endCaptures":{"1":{"name":"punctuation.separator.key-value.mapping.yaml"},"2":{"name":"invalid.illegal.expected-newline.yaml"}},"name":"meta.block-mapping.yaml","patterns":[{"include":"#block-node"}]},{"begin":"(?=(?:[^-\\\\]!\\"#%\\\\&'*,:>?@\\\\[\`{|}\\\\s]|[-:?]\\\\S)([^:\\\\s]|:\\\\S|\\\\s+(?![#\\\\s]))*\\\\s*:(\\\\s|$))","end":"(?=\\\\s*$|\\\\s+#|\\\\s*:(\\\\s|$))","patterns":[{"include":"#flow-scalar-plain-out-implicit-type"},{"begin":"[^-\\\\]!\\"#%\\\\&'*,:>?@\\\\[\`{|}\\\\s]|[-:?]\\\\S","beginCaptures":{"0":{"name":"entity.name.tag.yaml"}},"contentName":"entity.name.tag.yaml","end":"(?=\\\\s*$|\\\\s+#|\\\\s*:(\\\\s|$))","name":"string.unquoted.plain.out.yaml"}]},{"match":":(?=\\\\s|$)","name":"punctuation.separator.key-value.mapping.yaml"}]},"block-scalar":{"begin":"(?:(\\\\|)|(>))([1-9])?([-+])?(.*\\\\n?)","beginCaptures":{"1":{"name":"keyword.control.flow.block-scalar.literal.yaml"},"2":{"name":"keyword.control.flow.block-scalar.folded.yaml"},"3":{"name":"constant.numeric.indentation-indicator.yaml"},"4":{"name":"storage.modifier.chomping-indicator.yaml"},"5":{"patterns":[{"include":"#comment"},{"match":".+","name":"invalid.illegal.expected-comment-or-newline.yaml"}]}},"end":"^(?=\\\\S)|(?!\\\\G)","patterns":[{"begin":"^( +)(?! )","end":"^(?!\\\\1|\\\\s*$)","name":"string.unquoted.block.yaml"}]},"block-sequence":{"match":"(-)(?!\\\\S)","name":"punctuation.definition.block.sequence.item.yaml"},"comment":{"begin":"(?:^([\\\\t ]*)|[\\\\t ]+)(?=#\\\\p{print}*$)","beginCaptures":{"1":{"name":"punctuation.whitespace.comment.leading.yaml"}},"end":"(?!\\\\G)","patterns":[{"begin":"#","beginCaptures":{"0":{"name":"punctuation.definition.comment.yaml"}},"end":"\\\\n","name":"comment.line.number-sign.yaml"}]},"directive":{"begin":"^%","beginCaptures":{"0":{"name":"punctuation.definition.directive.begin.yaml"}},"end":"(?=$|[\\\\t ]+($|#))","name":"meta.directive.yaml","patterns":[{"captures":{"1":{"name":"keyword.other.directive.yaml.yaml"},"2":{"name":"constant.numeric.yaml-version.yaml"}},"match":"\\\\G(YAML)[\\\\t ]+(\\\\d+\\\\.\\\\d+)"},{"captures":{"1":{"name":"keyword.other.directive.tag.yaml"},"2":{"name":"storage.type.tag-handle.yaml"},"3":{"name":"support.type.tag-prefix.yaml"}},"match":"\\\\G(TAG)(?:[\\\\t ]+(!(?:[-0-9A-Za-z]*!)?)(?:[\\\\t ]+(!(?:%\\\\h{2}|[]!#$\\\\&-;=?-\\\\[_a-z~])*|(?![]!,\\\\[{}])(?:%\\\\h{2}|[]!#$\\\\&-;=?-\\\\[_a-z~])+))?)?"},{"captures":{"1":{"name":"support.other.directive.reserved.yaml"},"2":{"name":"string.unquoted.directive-name.yaml"},"3":{"name":"string.unquoted.directive-parameter.yaml"}},"match":"\\\\G(\\\\w+)(?:[\\\\t ]+(\\\\w+)(?:[\\\\t ]+(\\\\w+))?)?"},{"match":"\\\\S+","name":"invalid.illegal.unrecognized.yaml"}]},"flow-alias":{"captures":{"1":{"name":"keyword.control.flow.alias.yaml"},"2":{"name":"punctuation.definition.alias.yaml"},"3":{"name":"variable.other.alias.yaml"},"4":{"name":"invalid.illegal.character.anchor.yaml"}},"match":"((\\\\*))([^],/\\\\[{}\\\\s]+)([^],}\\\\s]\\\\S*)?"},"flow-collection":{"patterns":[{"include":"#flow-sequence"},{"include":"#flow-mapping"}]},"flow-mapping":{"begin":"\\\\{","beginCaptures":{"0":{"name":"punctuation.definition.mapping.begin.yaml"}},"end":"}","endCaptures":{"0":{"name":"punctuation.definition.mapping.end.yaml"}},"name":"meta.flow-mapping.yaml","patterns":[{"include":"#prototype"},{"match":",","name":"punctuation.separator.mapping.yaml"},{"include":"#flow-pair"}]},"flow-node":{"patterns":[{"include":"#prototype"},{"include":"#flow-alias"},{"include":"#flow-collection"},{"include":"#flow-scalar"}]},"flow-pair":{"patterns":[{"begin":"\\\\?","beginCaptures":{"0":{"name":"punctuation.definition.key-value.begin.yaml"}},"end":"(?=[],}])","name":"meta.flow-pair.explicit.yaml","patterns":[{"include":"#prototype"},{"include":"#flow-pair"},{"include":"#flow-node"},{"begin":":(?=\\\\s|$|[],\\\\[{}])","beginCaptures":{"0":{"name":"punctuation.separator.key-value.mapping.yaml"}},"end":"(?=[],}])","patterns":[{"include":"#flow-value"}]}]},{"begin":"(?=(?:[^-\\\\]!\\"#%\\\\&'*,:>?@\\\\[\`{|}\\\\s]|[-:?][^],\\\\[{}\\\\s])([^],:\\\\[{}\\\\s]|:[^],\\\\[{}\\\\s]|\\\\s+(?![#\\\\s]))*\\\\s*:(\\\\s|$))","end":"(?=\\\\s*$|\\\\s+#|\\\\s*:(\\\\s|$)|\\\\s*:[],\\\\[{}]|\\\\s*[],\\\\[{}])","name":"meta.flow-pair.key.yaml","patterns":[{"include":"#flow-scalar-plain-in-implicit-type"},{"begin":"[^-\\\\]!\\"#%\\\\&'*,:>?@\\\\[\`{|}\\\\s]|[-:?][^],\\\\[{}\\\\s]","beginCaptures":{"0":{"name":"entity.name.tag.yaml"}},"contentName":"entity.name.tag.yaml","end":"(?=\\\\s*$|\\\\s+#|\\\\s*:(\\\\s|$)|\\\\s*:[],\\\\[{}]|\\\\s*[],\\\\[{}])","name":"string.unquoted.plain.in.yaml"}]},{"include":"#flow-node"},{"begin":":(?=\\\\s|$|[],\\\\[{}])","captures":{"0":{"name":"punctuation.separator.key-value.mapping.yaml"}},"end":"(?=[],}])","name":"meta.flow-pair.yaml","patterns":[{"include":"#flow-value"}]}]},"flow-scalar":{"patterns":[{"include":"#flow-scalar-double-quoted"},{"include":"#flow-scalar-single-quoted"},{"include":"#flow-scalar-plain-in"}]},"flow-scalar-double-quoted":{"begin":"\\"","beginCaptures":{"0":{"name":"punctuation.definition.string.begin.yaml"}},"end":"\\"","endCaptures":{"0":{"name":"punctuation.definition.string.end.yaml"}},"name":"string.quoted.double.yaml","patterns":[{"match":"\\\\\\\\([ \\"/0LN\\\\\\\\_abefnprtv]|x\\\\d\\\\d|u\\\\d{4}|U\\\\d{8})","name":"constant.character.escape.yaml"},{"match":"\\\\\\\\\\\\n","name":"constant.character.escape.double-quoted.newline.yaml"}]},"flow-scalar-plain-in":{"patterns":[{"include":"#flow-scalar-plain-in-implicit-type"},{"begin":"[^-\\\\]!\\"#%\\\\&'*,:>?@\\\\[\`{|}\\\\s]|[-:?][^],\\\\[{}\\\\s]","end":"(?=\\\\s*$|\\\\s+#|\\\\s*:(\\\\s|$)|\\\\s*:[],\\\\[{}]|\\\\s*[],\\\\[{}])","name":"string.unquoted.plain.in.yaml"}]},"flow-scalar-plain-in-implicit-type":{"patterns":[{"captures":{"1":{"name":"constant.language.null.yaml"},"2":{"name":"constant.language.boolean.yaml"},"3":{"name":"constant.numeric.integer.yaml"},"4":{"name":"constant.numeric.float.yaml"},"5":{"name":"constant.other.timestamp.yaml"},"6":{"name":"constant.language.value.yaml"},"7":{"name":"constant.language.merge.yaml"}},"match":"(?:(null|Null|NULL|~)|([Yy]|yes|Yes|YES|[Nn]|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)|([-+]?0b[01_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[_\\\\h]+|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)|([-+]?(?:[0-9][0-9_]*)?\\\\.[.0-9]*(?:[Ee][-+][0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\\\.[0-9_]*|[-+]?\\\\.(?:inf|Inf|INF)|\\\\.(?:nan|NaN|NAN))|(\\\\d{4}-\\\\d{2}-\\\\d{2}|\\\\d{4}-\\\\d{1,2}-\\\\d{1,2}(?:[Tt]|[\\\\t ]+)\\\\d{1,2}:\\\\d{2}:\\\\d{2}(?:\\\\.\\\\d*)?(?:[\\\\t ]*Z|[-+]\\\\d{1,2}(?::\\\\d{1,2})?)?)|(=)|(<<))(?=\\\\s*$|\\\\s+#|\\\\s*:(\\\\s|$)|\\\\s*:[],\\\\[{}]|\\\\s*[],\\\\[{}])"}]},"flow-scalar-plain-out":{"patterns":[{"include":"#flow-scalar-plain-out-implicit-type"},{"begin":"[^-\\\\]!\\"#%\\\\&'*,:>?@\\\\[\`{|}\\\\s]|[-:?]\\\\S","end":"(?=\\\\s*$|\\\\s+#|\\\\s*:(\\\\s|$))","name":"string.unquoted.plain.out.yaml"}]},"flow-scalar-plain-out-implicit-type":{"patterns":[{"captures":{"1":{"name":"constant.language.null.yaml"},"2":{"name":"constant.language.boolean.yaml"},"3":{"name":"constant.numeric.integer.yaml"},"4":{"name":"constant.numeric.float.yaml"},"5":{"name":"constant.other.timestamp.yaml"},"6":{"name":"constant.language.value.yaml"},"7":{"name":"constant.language.merge.yaml"}},"match":"(?:(null|Null|NULL|~)|([Yy]|yes|Yes|YES|[Nn]|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)|([-+]?0b[01_]+|[-+]?0[0-7_]+|[-+]?(?:0|[1-9][0-9_]*)|[-+]?0x[_\\\\h]+|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)|([-+]?(?:[0-9][0-9_]*)?\\\\.[.0-9]*(?:[Ee][-+][0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\\\.[0-9_]*|[-+]?\\\\.(?:inf|Inf|INF)|\\\\.(?:nan|NaN|NAN))|(\\\\d{4}-\\\\d{2}-\\\\d{2}|\\\\d{4}-\\\\d{1,2}-\\\\d{1,2}(?:[Tt]|[\\\\t ]+)\\\\d{1,2}:\\\\d{2}:\\\\d{2}(?:\\\\.\\\\d*)?(?:[\\\\t ]*Z|[-+]\\\\d{1,2}(?::\\\\d{1,2})?)?)|(=)|(<<))(?=\\\\s*$|\\\\s+#|\\\\s*:(\\\\s|$))"}]},"flow-scalar-single-quoted":{"begin":"'","beginCaptures":{"0":{"name":"punctuation.definition.string.begin.yaml"}},"end":"'(?!')","endCaptures":{"0":{"name":"punctuation.definition.string.end.yaml"}},"name":"string.quoted.single.yaml","patterns":[{"match":"''","name":"constant.character.escape.single-quoted.yaml"}]},"flow-sequence":{"begin":"\\\\[","beginCaptures":{"0":{"name":"punctuation.definition.sequence.begin.yaml"}},"end":"]","endCaptures":{"0":{"name":"punctuation.definition.sequence.end.yaml"}},"name":"meta.flow-sequence.yaml","patterns":[{"include":"#prototype"},{"match":",","name":"punctuation.separator.sequence.yaml"},{"include":"#flow-pair"},{"include":"#flow-node"}]},"flow-value":{"patterns":[{"begin":"\\\\G(?![],}])","end":"(?=[],}])","name":"meta.flow-pair.value.yaml","patterns":[{"include":"#flow-node"}]}]},"node":{"patterns":[{"include":"#block-node"}]},"property":{"begin":"(?=[!\\\\&])","end":"(?!\\\\G)","name":"meta.property.yaml","patterns":[{"captures":{"1":{"name":"keyword.control.property.anchor.yaml"},"2":{"name":"punctuation.definition.anchor.yaml"},"3":{"name":"entity.name.type.anchor.yaml"},"4":{"name":"invalid.illegal.character.anchor.yaml"}},"match":"\\\\G((&))([^],/\\\\[{}\\\\s]+)(\\\\S+)?"},{"match":"\\\\G!(?:<(?:%\\\\h{2}|[]!#$\\\\&-;=?-\\\\[_a-z~])+>|(?:[-0-9A-Za-z]*!)?(?:%\\\\h{2}|[#$\\\\&-+\\\\--;=?-Z_a-z~])+|)(?=[\\\\t ]|$)","name":"storage.type.tag-handle.yaml"},{"match":"\\\\S+","name":"invalid.illegal.tag-handle.yaml"}]},"prototype":{"patterns":[{"include":"#comment"},{"include":"#property"}]}},"scopeName":"source.yaml","aliases":["yml"]}`));var n=[e];export{n as default};
|
package/web-dist/index.html
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
<meta name="theme-color" content="#030712" media="(prefers-color-scheme: dark)" />
|
|
8
8
|
<link rel="icon" type="image/svg+xml" href="/penguin-logo.svg" />
|
|
9
9
|
<title>PenguinHarness</title>
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-CmfumtXy.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Ywc2uM5u.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="root"></div>
|
package/dist/chunk-4SXGU7RO.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
// src/hmr/manifest.ts
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import fsp from "fs/promises";
|
|
4
|
-
import path from "path";
|
|
5
|
-
function str(value) {
|
|
6
|
-
return typeof value === "string" && value.length > 0 ? value : null;
|
|
7
|
-
}
|
|
8
|
-
async function readHarnessInfo(root) {
|
|
9
|
-
const manifest = await readManifest(root);
|
|
10
|
-
if (manifest === null) return null;
|
|
11
|
-
const bundles = {
|
|
12
|
-
platform: str(manifest.platform?.bundle),
|
|
13
|
-
cli: str(manifest.cli?.bundle),
|
|
14
|
-
web: str(manifest.web?.manifest)
|
|
15
|
-
};
|
|
16
|
-
if (bundles.platform === null && bundles.cli === null && bundles.web === null) return null;
|
|
17
|
-
const repo = str(manifest.source?.repo);
|
|
18
|
-
const revision = str(manifest.source?.revision);
|
|
19
|
-
return {
|
|
20
|
-
// Both halves or neither: half a provenance names nothing.
|
|
21
|
-
source: repo !== null && revision !== null ? { repo, revision } : null,
|
|
22
|
-
pushedAt: str(manifest.pushedAt),
|
|
23
|
-
bundles
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
async function readManifest(root) {
|
|
27
|
-
try {
|
|
28
|
-
const raw = await fsp.readFile(path.join(root, "hmr", "harness.json"), "utf8");
|
|
29
|
-
return JSON.parse(raw);
|
|
30
|
-
} catch {
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
async function resolveCliBundlePath(root) {
|
|
35
|
-
const manifest = await readManifest(root);
|
|
36
|
-
const bundle = manifest?.cli?.bundle;
|
|
37
|
-
if (typeof bundle !== "string" || bundle.length === 0) return null;
|
|
38
|
-
const hmrDir = path.join(root, "hmr");
|
|
39
|
-
const abs = path.resolve(hmrDir, bundle);
|
|
40
|
-
const withinHmrDir = abs === hmrDir || abs.startsWith(hmrDir + path.sep);
|
|
41
|
-
if (!withinHmrDir) return null;
|
|
42
|
-
return fs.existsSync(abs) ? abs : null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export {
|
|
46
|
-
readHarnessInfo,
|
|
47
|
-
readManifest,
|
|
48
|
-
resolveCliBundlePath
|
|
49
|
-
};
|
|
50
|
-
//# sourceMappingURL=chunk-4SXGU7RO.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/hmr/manifest.ts"],"sourcesContent":["/**\n * The harness.json shape (`<root>/hmr/harness.json`) and pure readers over it.\n *\n * Split out of host.ts so a reader never needs a running HmrHost: `penguin` is a\n * separate, short-lived process per invocation (see packages/cli/src/index.ts) — it\n * has no host instance to ask, only the data root's own harness.json on disk.\n * `resolveCliBundlePath` is that reader: a plain disk read, no HTTP, no boot, no\n * in-memory state.\n *\n * ONE atomic version, THREE independent artifacts: `platform`, `cli`, and `web` are\n * always written together by HmrHost's single merged upgrade path (see host.ts's\n * persistVersion), but each is content-addressed and stored on its own — a push to\n * POST /api/hmr/upgrade carries `platform` and `cli` as two separate single-file ESM\n * sources, so `cli.bundle` points at its OWN file, a different sha from\n * `platform.bundle`. They are read back together too (see host.ts's restore): a\n * partial record (e.g. `platform` present but `web` or `cli` missing) is never\n * trusted — the whole version is treated as absent instead of partially applied.\n */\nimport fs from \"node:fs\";\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { HarnessInfo } from \"@prismshadow/penguin-core\";\n\n/**\n * The committed on-disk record: a runtime restart resumes exactly this CODE (platform,\n * cli, web). It carries no STATE — a restart always boots the resumed platform bundle\n * against its own fresh initial context (bundle.context, see host.ts's PlatformBundle),\n * never a document parked from a previous run (see host.ts's module doc for why: a\n * parked doc's live-resource handles die with the process anyway, so resuming one could\n * only ever produce handles that fail to reclaim). Paths are relative to hmrDir\n * (`<root>/hmr`).\n */\nexport interface Manifest {\n platform?: { bundle: string };\n /**\n * The CLI's own bundle pointer — its own independent file (a different sha from\n * `platform.bundle`; the two are separately compiled artifacts), kept as its own\n * field so a reader that only wants \"which bundle does the CLI load right now\"\n * never has to reach into `platform` at all.\n */\n cli?: { bundle: string };\n /** One gzip(JSON.stringify({ files })) artifact, restored straight into memory. */\n web?: { manifest: string };\n /**\n * Files the pushed platform needs as REAL files on disk, unpacked under this directory\n * (relative to hmrDir). The web artifact stays in memory because it is only ever served\n * as bytes; an asset is something the platform hands to the OS — a native `.node` the\n * loader must resolve by path, a helper binary it execs — so it has to exist on the\n * filesystem, with its exec bit intact.\n */\n assets?: { dir: string };\n /**\n * Provenance of the push that produced this version, exactly as the pushing client sent\n * it (scripts/deploy.mjs fills it from its own checkout). Recorded, never executed or\n * resolved. Absent for a version pushed without it, and for every version committed\n * before the field existed — which is why every reader treats it as optional.\n */\n source?: { repo: string; revision: string };\n /** When this version was committed to the store (ISO 8601). Absent on older records. */\n pushedAt?: string;\n}\n\n/** A string field of an untrusted manifest, or null unless it is a non-empty string. */\nfunction str(value: unknown): string | null {\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\n/**\n * What the store has committed, for version reporting: provenance, commit time, and the\n * three artifact pointers. Null when nothing has ever been pushed to this root.\n *\n * Read defensively rather than cast: harness.json is written only by persistVersion, but a\n * truncated or hand-edited file must degrade a `penguin version` to missing fields, never\n * crash it. A record missing every artifact pointer counts as nothing pushed.\n */\nexport async function readHarnessInfo(root: string): Promise<HarnessInfo | null> {\n const manifest = await readManifest(root);\n if (manifest === null) return null;\n\n const bundles = {\n platform: str(manifest.platform?.bundle),\n cli: str(manifest.cli?.bundle),\n web: str(manifest.web?.manifest),\n };\n if (bundles.platform === null && bundles.cli === null && bundles.web === null) return null;\n\n const repo = str(manifest.source?.repo);\n const revision = str(manifest.source?.revision);\n return {\n // Both halves or neither: half a provenance names nothing.\n source: repo !== null && revision !== null ? { repo, revision } : null,\n pushedAt: str(manifest.pushedAt),\n bundles,\n };\n}\n\n/** Reads and parses `<root>/hmr/harness.json`; null when missing or corrupt (nothing committed yet). */\nexport async function readManifest(root: string): Promise<Manifest | null> {\n try {\n const raw = await fsp.readFile(path.join(root, \"hmr\", \"harness.json\"), \"utf8\");\n return JSON.parse(raw) as Manifest;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolves the current CLI bundle's absolute path from a data root, or null when\n * nothing has been pushed yet: a fresh root, a missing/corrupt manifest, no `cli`\n * entry, a `cli.bundle` that isn't a non-empty string (e.g. `{\"cli\":{}}` — a\n * hand-edited or truncated harness.json), a path that resolves outside `<root>/hmr/`\n * (defense in depth: `manifest.cli.bundle` is trusted content today, written only by\n * this same process's persistVersion, but a reader must never let a malformed or\n * malicious manifest value walk it out of the store directory via `..` segments), or\n * a referenced file that no longer exists (e.g. pruned from the store — see host.ts's\n * pruneStore). A null return means \"use the packaged default\" to every caller;\n * nothing here ever throws for an ordinary unconfigured or malformed root.\n */\nexport async function resolveCliBundlePath(root: string): Promise<string | null> {\n const manifest = await readManifest(root);\n const bundle = manifest?.cli?.bundle;\n if (typeof bundle !== \"string\" || bundle.length === 0) return null;\n const hmrDir = path.join(root, \"hmr\");\n const abs = path.resolve(hmrDir, bundle);\n const withinHmrDir = abs === hmrDir || abs.startsWith(hmrDir + path.sep);\n if (!withinHmrDir) return null;\n return fs.existsSync(abs) ? abs : null;\n}\n"],"mappings":";AAkBA,OAAO,QAAQ;AACf,OAAO,SAAS;AAChB,OAAO,UAAU;AA2CjB,SAAS,IAAI,OAA+B;AAC1C,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAUA,eAAsB,gBAAgB,MAA2C;AAC/E,QAAM,WAAW,MAAM,aAAa,IAAI;AACxC,MAAI,aAAa,KAAM,QAAO;AAE9B,QAAM,UAAU;AAAA,IACd,UAAU,IAAI,SAAS,UAAU,MAAM;AAAA,IACvC,KAAK,IAAI,SAAS,KAAK,MAAM;AAAA,IAC7B,KAAK,IAAI,SAAS,KAAK,QAAQ;AAAA,EACjC;AACA,MAAI,QAAQ,aAAa,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,KAAM,QAAO;AAEtF,QAAM,OAAO,IAAI,SAAS,QAAQ,IAAI;AACtC,QAAM,WAAW,IAAI,SAAS,QAAQ,QAAQ;AAC9C,SAAO;AAAA;AAAA,IAEL,QAAQ,SAAS,QAAQ,aAAa,OAAO,EAAE,MAAM,SAAS,IAAI;AAAA,IAClE,UAAU,IAAI,SAAS,QAAQ;AAAA,IAC/B;AAAA,EACF;AACF;AAGA,eAAsB,aAAa,MAAwC;AACzE,MAAI;AACF,UAAM,MAAM,MAAM,IAAI,SAAS,KAAK,KAAK,MAAM,OAAO,cAAc,GAAG,MAAM;AAC7E,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAcA,eAAsB,qBAAqB,MAAsC;AAC/E,QAAM,WAAW,MAAM,aAAa,IAAI;AACxC,QAAM,SAAS,UAAU,KAAK;AAC9B,MAAI,OAAO,WAAW,YAAY,OAAO,WAAW,EAAG,QAAO;AAC9D,QAAM,SAAS,KAAK,KAAK,MAAM,KAAK;AACpC,QAAM,MAAM,KAAK,QAAQ,QAAQ,MAAM;AACvC,QAAM,eAAe,QAAQ,UAAU,IAAI,WAAW,SAAS,KAAK,GAAG;AACvE,MAAI,CAAC,aAAc,QAAO;AAC1B,SAAO,GAAG,WAAW,GAAG,IAAI,MAAM;AACpC;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/db/database.ts","../src/db/migrations.ts","../src/db/schema.ts"],"sourcesContent":["/**\n * SQLite connection & initialization (node:sqlite DatabaseSync).\n *\n * Single process, single writer: a synchronous API is sufficient and avoids a connection\n * pool; WAL mode and foreign key constraints are enabled. Table-creation SQL runs on open\n * (idempotent), with no migration branches (product not yet released).\n */\nimport { mkdirSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { migrate } from \"./migrations.js\";\nimport { SCHEMA_SQL } from \"./schema.js\";\n\n// Fetch the runtime module via process.getBuiltinModule (node >=22.3): avoids static\n// resolution of `node:sqlite` by bundlers/vite (some tools' builtin lists don't yet\n// recognize this experimental module).\nconst sqlite = process.getBuiltinModule(\"node:sqlite\");\n\n/**\n * Opens an EXISTING database for a narrow write, without running any schema work.\n *\n * `openDatabase` migrates — CREATE TABLE, several ensureColumn calls, a DROP INDEX and a backfill.\n * That is right for the process that owns the database and wrong for a short-lived CLI writing\n * one row (`penguin auth token`): after `penguin update` the CLI on disk can be NEWER than the\n * running server, and migrating under a live server's prepared statements is not something a\n * token mint should do. The caller has already established that the file exists.\n */\nexport function openExistingDatabase(dbPath: string): DatabaseSync {\n const db = new sqlite.DatabaseSync(dbPath);\n db.exec(\"PRAGMA foreign_keys = ON;\");\n // Wait for the live server's write lock instead of failing SQLITE_BUSY at once.\n db.exec(\"PRAGMA busy_timeout = 5000;\");\n return db;\n}\n\n/**\n * Opens an EXISTING database for reading only — SQLite refuses every write on the connection,\n * so a reader cannot change the file even by mistake. For a question asked of a database some\n * other process owns (`penguin server status`, run over ssh by a controller): the answer must\n * not reshape that server's schema, and must not be able to. The caller has established that\n * the file exists; a read-only open of a missing file is an error, not a creation.\n */\nexport function openDatabaseReadOnly(dbPath: string): DatabaseSync {\n const db = new sqlite.DatabaseSync(dbPath, { readOnly: true });\n db.exec(\"PRAGMA busy_timeout = 5000;\");\n return db;\n}\n\n/** Open (creating if necessary) the database: ensure the parent directory exists, set PRAGMAs, run table creation. */\nexport function openDatabase(dbPath: string): DatabaseSync {\n if (dbPath !== \":memory:\") {\n mkdirSync(path.dirname(dbPath), { recursive: true });\n }\n const db = new sqlite.DatabaseSync(dbPath);\n db.exec(\"PRAGMA journal_mode = WAL;\");\n // A second connection (the CLI minting a token while the server runs) waits for the write\n // lock rather than failing SQLITE_BUSY at once.\n db.exec(\"PRAGMA busy_timeout = 5000;\");\n db.exec(\"PRAGMA foreign_keys = ON;\");\n db.exec(SCHEMA_SQL);\n // Columns added to the schema after a web.db was formed: CREATE TABLE IF NOT EXISTS never\n // touches an existing table, so they are ALTERed in here. Keep the list in sync with\n // schema.ts; drop entries only in a release allowed to break existing web.db files.\n ensureColumn(db, \"sessions\", \"client\", \"TEXT\");\n ensureColumn(db, \"sessions\", \"has_trace\", \"INTEGER NOT NULL DEFAULT 0\");\n ensureColumn(db, \"sessions\", \"fork_count\", \"INTEGER NOT NULL DEFAULT 0\");\n ensureColumn(db, \"sessions\", \"thinking_level\", \"TEXT\");\n // A v0.2.0 web.db has auth_sessions without `via`; CREATE TABLE IF NOT EXISTS never adds a\n // column, so every INSERT would fail \"no column named via\" until this runs.\n ensureColumn(db, \"auth_sessions\", \"via\", \"TEXT\");\n ensureColumn(db, \"trace_files\", \"page_stats\", \"TEXT\");\n ensureColumn(db, \"messaging_bindings\", \"line_per_message\", \"INTEGER NOT NULL DEFAULT 0\");\n ensureColumn(db, \"messaging_bindings\", \"final_reply_only\", \"INTEGER NOT NULL DEFAULT 0\");\n // DEFAULT 1, unlike every other added flag here: this one's default is the CORRECTED\n // behaviour, not the previous one. Sending the model's Markdown as characters was the\n // defect, so an existing binding starts rendering it — a visible change to every relayed\n // message, recorded in the release's backward-compatibility entry.\n ensureColumn(db, \"messaging_bindings\", \"render_markdown\", \"INTEGER NOT NULL DEFAULT 1\");\n ensureColumn(db, \"messaging_bindings\", \"last_inbound_message_id\", \"TEXT\");\n // Superseded by idx_usage_session_ts (session_id, ts), which SCHEMA_SQL just created on\n // this database: the old index is a strict prefix of it, so every query it served is\n // served identically. Dropping is safe — an index is derived, never data.\n db.exec(\"DROP INDEX IF EXISTS idx_usage_session\");\n // The uniqueness index of the old binding model, where a bot account belonged to one\n // Session forever and a second Session could not even SAVE its credentials. Enabling a\n // connection is the binding now, so exclusivity is checked per enable (the state route's\n // 409 account_enabled_elsewhere) and the same account may sit saved on many Sessions —\n // which this index would reject. SCHEMA_SQL has just created idx_messaging_by_account\n // over the same columns, so the by-account lookup stays indexed. Dropping is safe: an\n // index is derived, never data. It is however ONE-WAY — an older build recreates the\n // unique index on open, which fails outright once duplicate (channel, account_id) rows\n // exist, so a downgrade after two Sessions saved the same bot needs those rows removed.\n db.exec(\"DROP INDEX IF EXISTS idx_messaging_account\");\n upgradeLastActiveAt(db);\n // The runtime's own open is the ONLY place a restart-only migration may be applied: it\n // owns the process, so there is no boot to roll back out from under it.\n migrate(db);\n return db;\n}\n\n/**\n * One-time `sessions.last_active_at` upgrade, ALTER + backfill in a single transaction.\n *\n * The backfill runs **only in the open that actually adds the column** (SQLite's ALTER\n * TABLE ADD COLUMN is transactional, so a crash mid-upgrade rolls back to \"no column\" and\n * the next open redoes both halves — never a half-migrated table, and never a full\n * `sessions` scan on the millions of opens that follow). Legacy rows take the session's\n * most recent request timestamp — usage_records, covered by idx_usage_session_ts, the\n * closest persisted proxy for \"last Trace activity\" — else their own created_at.\n */\nfunction upgradeLastActiveAt(db: DatabaseSync): void {\n db.exec(\"BEGIN\");\n try {\n if (ensureColumn(db, \"sessions\", \"last_active_at\", \"TEXT\")) {\n db.exec(\n `UPDATE sessions SET last_active_at = COALESCE(\n (SELECT MAX(ts) FROM usage_records WHERE usage_records.session_id = sessions.session_id),\n created_at\n )`,\n );\n }\n db.exec(\"COMMIT\");\n } catch (err) {\n db.exec(\"ROLLBACK\");\n throw err;\n }\n}\n\n/**\n * Idempotent per-column upgrade for databases formed before the column existed.\n * Returns whether this call actually ALTERed the table (false = the column was already\n * there), so a caller can gate one-time backfill work on it.\n */\nexport function ensureColumn(\n db: DatabaseSync,\n table: string,\n column: string,\n ddl: string,\n): boolean {\n const cols = db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[];\n if (cols.some((c) => c.name === column)) return false;\n db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${ddl}`);\n return true;\n}\n","/**\n * Ordered, versioned schema migrations.\n *\n * `schema.ts` declares the shape a FRESH database is created with. It cannot express a\n * CHANGE — `CREATE TABLE IF NOT EXISTS` only ever says \"should exist\", so re-running it\n * converges a database toward the current declaration without ever knowing, or recording,\n * which state it came from. That is why a build could not tell a 0.2.4 database from a\n * 0.2.7 one, and why the only safe change was an additive one.\n *\n * A migration says what CHANGED, runs once, and leaves the database stamped with how far\n * it has come (`PRAGMA user_version`). Two rules make that stamp trustworthy:\n *\n * FROZEN DDL. A migration spells out its own SQL and never imports SCHEMA_SQL. Referring\n * to the live declaration would make an old migration silently mean something new every\n * time the schema moves, which is the property that makes migrations auditable at all.\n *\n * ONE WAY, IN ORDER. Versions are contiguous from 1 and never renumbered or rewritten\n * once released — a database that already stamped version N will never run N again, so\n * editing N only changes what NEW databases get, and silently forks the two.\n *\n * `swapSafe` is this codebase's extra axis, and it exists because of hot updates. A\n * pushed platform boots against a live database and is ROLLED BACK to its predecessor if\n * it fails; the predecessor then runs on whatever the migration already did. Additive\n * work survives that (the older build does not know the new table, so it never touches\n * it) — narrowing work does not. Anything that drops, retypes, constrains, or reshapes\n * is `swapSafe: false` and must be refused on the swap path rather than half-applied\n * (see `migrate`'s `swapPath` option).\n *\n * DOWN, AND WHO DOES NOT CALL IT. Every migration declares an undo — or declares `null`\n * to say it has none, which is a decision the author has to make rather than omit. What\n * `down` is NOT is the hot-update rollback mechanism. When a pushed platform fails to boot\n * the runtime reverts to its PREDECESSOR and the schema stays where the migration left it:\n * undoing DDL inside a process whose boot just failed would run destructive statements\n * against a half-known state, and `swapSafe` exists precisely so that not undoing is safe —\n * the predecessor does not know the new table or column, so it never touches it. `down` is\n * an operator's tool, called deliberately (`rollbackTo`), never by the swap.\n *\n * It is also LOSSY by nature: undoing \"add a table\" drops that table with its rows in it.\n * Each `down` below names what its rows were.\n *\n * ADOPTION. Databases created before this file existed are all stamped 0 while sitting in\n * genuinely different shapes, so a migration must tolerate finding its work already done.\n * That is not only a version-1 concern: `schema.ts` still DECLARES the current shape in\n * full and `openDatabase` still runs its ensureColumn list, so a database can arrive at a\n * migration with that migration's work already applied by the declarative track. Until\n * SCHEMA_SQL is frozen to a baseline and every later change is a migration, every\n * migration here stays idempotent — `IF NOT EXISTS`, or the same `ensureColumn` guard the\n * declarative track uses. Once that double track is gone, a bare `CREATE TABLE` that\n * fails loudly becomes the point.\n */\nimport type { DatabaseSync } from \"node:sqlite\";\nimport { ensureColumn } from \"./database.js\";\n\nexport interface Migration {\n /** Contiguous from 1. Never renumbered, never edited once released. */\n version: number;\n /** Kebab-case, names the change — read in logs and in the stamp's history. */\n name: string;\n /**\n * May this run while a pushed platform boots? True only for strictly additive work,\n * which a rollback to the previous platform survives. See the module doc.\n */\n swapSafe: boolean;\n /** Applied inside a transaction; throw to abort and leave the version unchanged. */\n up: (db: DatabaseSync) => void;\n /**\n * Undoes `up`, or null when this migration cannot be undone — required, not optional, so\n * \"there is no undo\" is something the author states rather than forgets.\n *\n * Never called by the swap path (see the module doc): a failed boot reverts the platform,\n * not the schema. Reached only through `rollbackTo`, and destructive by nature — dropping\n * a table takes its rows with it.\n */\n down: ((db: DatabaseSync) => void) | null;\n}\n\nexport const MIGRATIONS: readonly Migration[] = [\n {\n version: 1,\n name: \"messaging-bindings\",\n // Purely additive: one new table plus three indexes, no column of any existing table\n // touched. A platform that rolls back to a predecessor without messaging simply never\n // queries them.\n swapSafe: true,\n up(db) {\n // 0.2.4 → 0.2.7. Frozen copy of the DDL as of 0.2.7; do not re-derive from schema.ts.\n // IF NOT EXISTS only because version 1 adopts unstamped databases, which may already\n // be at 0.2.7 (see the module doc's ADOPTION note).\n db.exec(`\n CREATE TABLE IF NOT EXISTS messaging_bindings (\n session_id TEXT NOT NULL,\n channel TEXT NOT NULL,\n account_id TEXT NOT NULL,\n config_json TEXT NOT NULL,\n enabled INTEGER NOT NULL DEFAULT 0,\n line_per_message INTEGER NOT NULL DEFAULT 0,\n last_chat_id TEXT,\n last_chat_is_direct INTEGER NOT NULL DEFAULT 1,\n last_inbound_message_id TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (session_id, channel)\n );\n CREATE INDEX IF NOT EXISTS idx_messaging_by_account ON messaging_bindings(channel, account_id);\n CREATE INDEX IF NOT EXISTS idx_auth_sessions_expires ON auth_sessions(expires_at);\n CREATE INDEX IF NOT EXISTS idx_auth_sessions_user ON auth_sessions(user_id);\n `);\n },\n // LOSES every messaging binding: the channel credentials, the enabled flag and the\n // last-chat memory all live in the table this drops. The auth_sessions indexes are\n // derived and cost nothing to lose.\n down(db) {\n db.exec(`\n DROP INDEX IF EXISTS idx_auth_sessions_user;\n DROP INDEX IF EXISTS idx_auth_sessions_expires;\n DROP INDEX IF EXISTS idx_messaging_by_account;\n DROP TABLE IF EXISTS messaging_bindings;\n `);\n },\n },\n {\n version: 2,\n name: \"messaging-delivery-flags\",\n // Two columns with defaults on an existing table: a rollback to a predecessor that\n // does not know them leaves them at their defaults and reads nothing.\n swapSafe: true,\n up(db) {\n // 0.2.7 → 0.2.8. ensureColumn rather than a bare ALTER because the declarative\n // track may already have added these (see the module doc's ADOPTION note).\n ensureColumn(db, \"messaging_bindings\", \"final_reply_only\", \"INTEGER NOT NULL DEFAULT 0\");\n ensureColumn(db, \"messaging_bindings\", \"render_markdown\", \"INTEGER NOT NULL DEFAULT 1\");\n },\n // LOSES both delivery preferences on every binding; the bindings themselves survive.\n // Dropped in reverse order for symmetry with `up`. Neither column is indexed, which is\n // what lets SQLite drop them at all.\n down(db) {\n db.exec(\"ALTER TABLE messaging_bindings DROP COLUMN render_markdown\");\n db.exec(\"ALTER TABLE messaging_bindings DROP COLUMN final_reply_only\");\n },\n },\n {\n version: 3,\n name: \"drop-goal-state\",\n // Narrowing: drops a table. A pushed platform rolled back to 0.2.9 mid-process would\n // prepare its goal statements against a table that is gone (its declarative track only\n // runs at the runtime's own open, never at a platform boot), so this is the first\n // restart-only migration: refused on the swap path, applied by the runtime's open.\n swapSafe: false,\n up(db) {\n // 0.2.9 → 0.2.10. goal_state held goal mode's run state, one row per goal run, read\n // back only for the chat page's goal banner; the goal plugin's GOAL.json in the\n // Session scratchpad is that record now (see runtime/goal-events.ts). IF EXISTS only\n // because a database this build created never had the table.\n db.exec(`\n DROP INDEX IF EXISTS idx_goal_session;\n DROP TABLE IF EXISTS goal_state;\n `);\n },\n // Recreates the table exactly as 0.2.9 declared it — EMPTY. LOSES every goal run ever\n // recorded (objective, status, budget, used, rounds per run): the rows only ever fed the\n // banner of a finished goal, and a build with this migration reads the goal file instead.\n down(db) {\n db.exec(`\n CREATE TABLE IF NOT EXISTS goal_state (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL,\n project_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n objective TEXT NOT NULL,\n status TEXT NOT NULL,\n budget INTEGER NOT NULL,\n used INTEGER NOT NULL DEFAULT 0,\n rounds INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS idx_goal_session ON goal_state(session_id);\n `);\n },\n },\n {\n version: 4,\n name: \"machines\",\n // Three new tables, nothing existing touched: a platform rolled back to one without\n // machines never queries them. Swap-safe on its own; a database still behind\n // drop-goal-state is refused whole by that one first, which is the rule.\n swapSafe: true,\n up(db) {\n // Frozen copy of the DDL as of the machines feature; do not re-derive from schema.ts.\n // IF NOT EXISTS because the declarative track may already have created them (ADOPTION).\n db.exec(`\n CREATE TABLE IF NOT EXISTS machine (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n machine_id TEXT NOT NULL\n );\n CREATE TABLE IF NOT EXISTS machines (\n address TEXT PRIMARY KEY,\n machine_id TEXT,\n version TEXT,\n installed_at TEXT,\n session_pid INTEGER,\n remote_port INTEGER,\n platform TEXT\n );\n CREATE TABLE IF NOT EXISTS machine_project (\n project_id TEXT PRIMARY KEY REFERENCES projects(project_id) ON DELETE CASCADE,\n addresses TEXT NOT NULL\n );\n `);\n },\n // LOSES this server's own machine id (every stored reference to it on other machines\n // then points at nothing), what was installed where, the sessions held, and which\n // machines each Project used.\n down(db) {\n db.exec(`\n DROP TABLE IF EXISTS machine_project;\n DROP TABLE IF EXISTS machines;\n DROP TABLE IF EXISTS machine;\n `);\n },\n },\n];\n\n/** The highest version this build knows how to reach. */\nexport const LATEST_VERSION: number = MIGRATIONS.reduce((n, m) => Math.max(n, m.version), 0);\n\n/** How far this database has been migrated. Unstamped (pre-migrations) databases read 0. */\nexport function schemaVersion(db: DatabaseSync): number {\n const row = db.prepare(\"PRAGMA user_version\").get() as { user_version: number } | undefined;\n return row?.user_version ?? 0;\n}\n\nexport class IrreversibleMigrationError extends Error {\n constructor(readonly migration: Migration) {\n super(\n `migration ${migration.version} (${migration.name}) declares no down and cannot be rolled back. ` +\n `Restore the database from a backup taken before it was applied.`,\n );\n this.name = \"IrreversibleMigrationError\";\n }\n}\n\nexport class RestartRequiredError extends Error {\n constructor(readonly migration: Migration) {\n super(\n `migration ${migration.version} (${migration.name}) is restart-only and cannot be applied ` +\n `while a pushed platform boots: it is not safe to leave behind if this boot is rolled back. ` +\n `Restart the runtime on a build that carries it, then push again.`,\n );\n this.name = \"RestartRequiredError\";\n }\n}\n\n/**\n * Applies every migration this database has not reached yet, in order.\n *\n * Each migration and its version stamp commit together, so an interrupted run leaves the\n * database at the last version that fully applied — never half-migrated. Already-current\n * databases do no work and touch nothing.\n *\n * `swapPath` marks the caller as a booting pushed platform: the first pending migration\n * that is not `swapSafe` throws RestartRequiredError BEFORE anything is applied, so the\n * push is refused whole rather than partially landed.\n */\nexport function migrate(\n db: DatabaseSync,\n { swapPath = false }: { swapPath?: boolean } = {},\n): { from: number; to: number; applied: readonly string[] } {\n const from = schemaVersion(db);\n const pending = [...MIGRATIONS]\n .sort((a, b) => a.version - b.version)\n .filter((m) => m.version > from);\n if (swapPath) {\n const blocked = pending.find((m) => !m.swapSafe);\n if (blocked) throw new RestartRequiredError(blocked);\n }\n const applied: string[] = [];\n for (const m of pending) {\n db.exec(\"BEGIN\");\n try {\n m.up(db);\n // PRAGMA takes no bound parameter; the value is this file's own integer literal.\n db.exec(`PRAGMA user_version = ${m.version}`);\n db.exec(\"COMMIT\");\n } catch (err) {\n db.exec(\"ROLLBACK\");\n throw new Error(`migration ${m.version} (${m.name}) failed: ${String(err)}`, { cause: err });\n }\n applied.push(m.name);\n }\n return { from, to: schemaVersion(db), applied };\n}\n\n/**\n * Reverts the database DOWN to `targetVersion`, newest migration first.\n *\n * An operator's tool, and a destructive one — see each migration's `down` for what its rows\n * were. Deliberately not reachable from the swap path: a failed platform boot reverts the\n * PLATFORM, never the schema (module doc).\n *\n * Refused whole, before anything runs, if any migration in range declares no `down`: a\n * partial rollback would leave the database at a version whose meaning nobody wrote down.\n * Each `down` commits with its version stamp, so an interrupted run stops at a real version.\n */\nexport function rollbackTo(\n db: DatabaseSync,\n targetVersion: number,\n): { from: number; to: number; reverted: readonly string[] } {\n const from = schemaVersion(db);\n if (targetVersion < 0) throw new Error(`target version ${targetVersion} is negative`);\n if (targetVersion > from) {\n throw new Error(`database is at version ${from}, which is already below ${targetVersion}`);\n }\n const toRevert = [...MIGRATIONS]\n .filter((m) => m.version > targetVersion && m.version <= from)\n .sort((a, b) => b.version - a.version);\n const blocked = toRevert.find((m) => m.down === null);\n if (blocked) throw new IrreversibleMigrationError(blocked);\n\n const reverted: string[] = [];\n for (const m of toRevert) {\n db.exec(\"BEGIN\");\n try {\n m.down!(db);\n // Versions are contiguous, so the predecessor of `m` is exactly m.version - 1.\n db.exec(`PRAGMA user_version = ${m.version - 1}`);\n db.exec(\"COMMIT\");\n } catch (err) {\n db.exec(\"ROLLBACK\");\n throw new Error(`rollback of migration ${m.version} (${m.name}) failed: ${String(err)}`, {\n cause: err,\n });\n }\n reverted.push(m.name);\n }\n return { from, to: schemaVersion(db), reverted };\n}\n","/**\n * SQLite table-creation SQL.\n *\n * SQLite stores only indexes and aggregates: users / login sessions / Project authorization /\n * Agent & Session indexes / usage summaries / error records / UI preferences. Agent State,\n * Trace, and Workspace still follow the local directory-based storage rules.\n * Product not yet released: no migration branches — everything is CREATE IF NOT EXISTS, formed\n * once. The one exception: columns added to an existing table after release of a web.db are\n * ALTERed in by the idempotent per-column guard in database.ts (ensureColumn), since CREATE\n * TABLE IF NOT EXISTS never touches an existing table. A *reshaped* index follows the same\n * rule under a new name, with the superseded one dropped on open (idx_usage_session →\n * idx_usage_session_ts): CREATE INDEX IF NOT EXISTS never rebuilds an existing index either.\n */\n\nexport const SCHEMA_SQL = `\nCREATE TABLE IF NOT EXISTS users (\n user_id TEXT PRIMARY KEY, -- semantic id doubles as login name: ^[a-z][a-z0-9_-]{1,31}$\n password_hash TEXT NOT NULL, -- scrypt$N$r$p$salt$hash(base64)\n is_admin INTEGER NOT NULL DEFAULT 0, -- 1 for the built-in admin (seeded at startup)\n password_is_initial INTEGER NOT NULL DEFAULT 0, -- 1=initial password (seeded/admin-set); cleared once the user changes it\n created_at TEXT NOT NULL\n);\n-- Server-side sessions backing the HttpOnly cookie: the cookie carries a 32-byte random\n-- token, the row stores only its sha256. A session outlives a restart (it is on disk), which\n-- is why the model is a row and not a signed statement.\nCREATE TABLE IF NOT EXISTS auth_sessions (\n token_hash TEXT PRIMARY KEY, -- sha256(token) hex; the cookie stores only the raw token\n user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL, -- 30-day sliding renewal, topped up in place when <29 days remain\n via TEXT -- 'password'|'desktop'|'setup'|'cli'; NULL = legacy (password)\n);\n-- deleteExpired runs on every login and mint, deleteByUser on every admin reset; without\n-- these both scan the whole table.\nCREATE INDEX IF NOT EXISTS idx_auth_sessions_expires ON auth_sessions(expires_at);\nCREATE INDEX IF NOT EXISTS idx_auth_sessions_user ON auth_sessions(user_id);\n\nCREATE TABLE IF NOT EXISTS projects (\n project_id TEXT PRIMARY KEY, -- directory name doubles as id; display name lives in project_config.toml\n owner_user_id TEXT NOT NULL REFERENCES users(user_id),\n created_at TEXT NOT NULL\n);\nCREATE TABLE IF NOT EXISTS project_members ( -- member grants only; owners are not stored here\n project_id TEXT NOT NULL REFERENCES projects(project_id) ON DELETE CASCADE,\n user_id TEXT NOT NULL REFERENCES users(user_id) ON DELETE CASCADE,\n created_at TEXT NOT NULL,\n PRIMARY KEY (project_id, user_id)\n);\nCREATE TABLE IF NOT EXISTS agents ( -- index only; name/description live in system_config.yaml\n project_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (project_id, agent_id)\n);\nCREATE TABLE IF NOT EXISTS sessions (\n session_id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n provider TEXT NOT NULL, -- provider group of the session model (paired with model_id as a model reference)\n model_id TEXT NOT NULL, -- upstream model id (sent to AgentHub as-is; never concatenate <provider>/<id>)\n workspace TEXT NOT NULL,\n approval_mode TEXT NOT NULL DEFAULT 'allow-all', -- allow-all|deny-all|read-only|always-ask\n thinking_level TEXT, -- level pinned for THIS session (none|low|medium|high|xhigh|max); NULL=not pinned, runs follow the Agent config\n title TEXT, -- auto-generated by the model after the first exchange; NULL=not yet (frontend shows \"New chat\")\n archived_at TEXT, -- archive time; NULL=not archived (shown by default; archived ones move under \"Archived\")\n client TEXT, -- creating client: 'web' (created via the Web App) | 'cli' (adopted from a CLI Trace); NULL = legacy row, treated as web\n has_trace INTEGER NOT NULL DEFAULT 0, -- cache: a Trace record exists (set at task start / adoption / subagent registration; lazily backfilled by list hydration)\n fork_count INTEGER NOT NULL DEFAULT 0, -- monotonically allocated child-fork number for this source Session; never decremented when a fork is deleted\n last_active_at TEXT, -- last activity this server drove for the session (ISO; stamped once when a run starts and once when it ends, initialized to created_at); monotonic, never moves backwards; NULL only before openDatabase's one-time backfill\n created_at TEXT NOT NULL\n); -- the schedule/subagent SOURCE is NOT stored: session_meta in the Trace is the single source of truth (see runtime/session-sources.ts); \"client\" is a different, DB-only axis (who created the row)\nCREATE INDEX IF NOT EXISTS idx_sessions_agent_created ON sessions(project_id, agent_id, created_at DESC);\nCREATE TABLE IF NOT EXISTS usage_records (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ts TEXT NOT NULL,\n date TEXT NOT NULL, -- local date yyyy-mm-dd (aggregation key; same convention as Trace date directories)\n project_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n session_id TEXT NOT NULL, -- top-level Session (child-session usage counts toward its main Session)\n origin_session_id TEXT, -- immediate source child Session (last of the origin chain); NULL=main session\n provider TEXT NOT NULL, -- provider group: paired with model_id as the attribution key; always GROUP BY provider, model_id\n model_id TEXT NOT NULL, -- upstream model id (paired with provider; same model_id across providers aggregates separately)\n cache_read INTEGER NOT NULL,\n cache_write INTEGER NOT NULL,\n output INTEGER NOT NULL,\n total INTEGER NOT NULL, -- from token_usage.request (one row per Request)\n status TEXT NOT NULL DEFAULT 'completed' -- request outcome: completed=success (with tokens); others=failure (0 tokens, for success rate)\n); -- cost is not stored: computed at query time from current pricing\nCREATE INDEX IF NOT EXISTS idx_usage_project_date ON usage_records(project_id, date);\nCREATE INDEX IF NOT EXISTS idx_usage_session_ts ON usage_records(session_id, ts); -- (session_id) alone was idx_usage_session (dropped on open): ts makes the last_active_at backfill's MAX(ts) a covering index scan instead of a random row lookup per usage record\nCREATE TABLE IF NOT EXISTS error_records ( -- server-side error capture (the Costs page's \"Errors\" tab)\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ts TEXT NOT NULL,\n date TEXT NOT NULL, -- local date yyyy-mm-dd (same convention as usage_records)\n project_id TEXT, -- nullable: sign-in/registration and process-level errors have no Project context\n agent_id TEXT,\n session_id TEXT,\n source TEXT NOT NULL, -- http | session | usage | title | subagent | process | llm | environment | compaction | schedule | messaging\n kind TEXT NOT NULL, -- expected (HttpError, business 4xx) | unexpected (500/runtime)\n code TEXT NOT NULL, -- HttpError.code / internal / session_run_failed / ...\n status INTEGER, -- HTTP status code; NULL for non-HTTP sources\n message TEXT NOT NULL -- truncated to 500 chars (no stack stored: stacks go to logs only)\n);\nCREATE INDEX IF NOT EXISTS idx_error_project_date ON error_records(project_id, date);\nCREATE TABLE IF NOT EXISTS schedule_state ( -- schedule runtime state (files are declarative intent; the system never writes back)\n project_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n name TEXT NOT NULL, -- file name (without .toml) doubles as the identifier\n creator_user_id TEXT, -- creator (recorded on API create; hand-edited file reconciliation falls back to the Project owner)\n start_at_ms INTEGER NOT NULL, -- defines identity: a start_at change counts as a new task instance, resetting fire state\n def_hash TEXT NOT NULL, -- file content fingerprint: any change clears the invalid flag (the file takes effect again after edits)\n last_slot_ms INTEGER, -- latest due slot already consumed (advances on fire or skip; no re-fire across restarts)\n last_fired_at TEXT, -- last actual fire time (for display)\n fired_once INTEGER NOT NULL DEFAULT 0, -- one-shot task has fired\n missed INTEGER NOT NULL DEFAULT 0, -- one-shot task was missed (flagged by startup/registration reconciliation; not backfilled)\n invalid_reason TEXT, -- invalidation reason (e.g. the bound Session was deleted); NULL=healthy\n PRIMARY KEY (project_id, agent_id, name)\n);\nCREATE TABLE IF NOT EXISTS messaging_bindings ( -- Session ↔ messaging-channel bot bindings (runtime/messaging/ holds the long connections)\n session_id TEXT NOT NULL, -- a Session keeps at most one saved config PER channel (composite PK below); at most ONE of them is enabled at a time (route-enforced, 409 another_channel_enabled), and enabling is what binds the account to this Session\n channel TEXT NOT NULL, -- messaging channel discriminator ('feishu' | 'telegram' | 'qq' | 'wechat')\n account_id TEXT NOT NULL, -- channel-scoped bot/app identity (feishu: app_id; telegram: the bot token's numeric id; qq: the bot's App ID; wechat: the scanned bot id); NOT unique — any number of Sessions may keep the same account saved side by side, each with its own config and last-chat memory; only ENABLING is exclusive (route-enforced, 409 account_enabled_elsewhere: one account has one event stream, two live connections on it would race)\n config_json TEXT NOT NULL, -- channel-specific credentials/config JSON (feishu: appId/appSecret/baseDomain; telegram: botToken; qq: appId/appSecret; wechat: botId/botToken/baseUrl/userId); secrets plaintext at rest (same trade-off as the proxy address in server_settings), masked at every API surface; a cleared secret is stored as \"\" (the row and its account identity stay)\n enabled INTEGER NOT NULL DEFAULT 0, -- INTENT state (the connection's runtime status stays in memory): new bindings start disabled — saving credentials never opens a connection, the explicit state toggle does\n line_per_message INTEGER NOT NULL DEFAULT 0, -- delivery preference: 1 = every non-blank line of a relayed assistant reply is sent as its own message (a reply written as spoken lines arrives as spoken lines), 0 = one message per reply. Channel-agnostic, so it lives here rather than in config_json, which each connector owns\n final_reply_only INTEGER NOT NULL DEFAULT 0, -- delivery preference: 1 = a run's intermediate assistant messages are held and only its LAST completed one is sent, at the run's end, 0 = every completed assistant message mirrors the moment it completes. Channel-agnostic like the column above and independent of it: with both set, that one final text is the one split per line\n render_markdown INTEGER NOT NULL DEFAULT 1, -- delivery preference: 1 = a relayed reply's Markdown is rendered in the channel's own markup (Telegram HTML, a Feishu card, QQ markdown, the subset WeChat itself reads), 0 = the reply's characters are sent as written. DEFAULT 1 because the model writes Markdown and showing it raw was the defect; the per-channel subsets and the plain-text fallback live in the connectors\n last_chat_id TEXT, -- most recent inbound chat (NULL until the bot is messaged once; replies and test messages target it); CONNECTOR-OPAQUE, not necessarily a bare chat id — telegram appends the forum topic (see telegram-connector.ts chatRefOf), so only that channel's connector may interpret it\n last_chat_is_direct INTEGER NOT NULL DEFAULT 1, -- 1 = direct chat (reply by chat id), 0 = group chat (prefer reply-to-message)\n last_inbound_message_id TEXT, -- channel id of the most recently PROCESSED inbound message (NULL until one arrives); the durable half of the bridge's redelivery guard, so a channel replaying an event this server already turned into a Task after a restart does not run it twice\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (session_id, channel)\n);\nCREATE INDEX IF NOT EXISTS idx_messaging_by_account ON messaging_bindings(channel, account_id); -- serves the enable guard's by-account lookup; deliberately NOT unique, unlike its predecessor idx_messaging_account (dropped on open) which made an account exclusive to one Session forever\nCREATE TABLE IF NOT EXISTS ui_prefs (\n user_id TEXT PRIMARY KEY REFERENCES users(user_id) ON DELETE CASCADE,\n prefs_json TEXT NOT NULL -- {theme?, lastProjectId?, ...} free-form JSON\n);\nCREATE TABLE IF NOT EXISTS server_settings ( -- admin-level server-global settings (GET/PUT /api/admin/settings)\n key TEXT PRIMARY KEY, -- setting name, e.g. 'proxy_for_app'\n value TEXT NOT NULL -- JSON-encoded value; an absent row means the setting's built-in default\n);\nCREATE TABLE IF NOT EXISTS machine ( -- this server's OWN identity, minted here and never assigned from outside (what 'penguin server status' reports)\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1), -- exactly one row, enforced by the schema rather than by convention\n machine_id TEXT NOT NULL -- 16 base64url characters; what every stored reference to this machine points at\n);\nCREATE TABLE IF NOT EXISTS machines ( -- one row per machine this server installed on or reached (machines/service.ts)\n address TEXT PRIMARY KEY, -- 'ssh:<alias>'\n machine_id TEXT, -- that machine's own id, once heard\n version TEXT, -- what this server last installed there; NULL = never\n installed_at TEXT,\n session_pid INTEGER, -- the ssh session this server holds to it; what a successor App closes after a hot swap\n remote_port INTEGER, -- its server's port over there, as of the last connect\n platform TEXT -- linux | darwin | win32, as the install found it: the shell dialect the status probe speaks to it in\n);\nCREATE TABLE IF NOT EXISTS machine_project ( -- which machines a Project uses; no row = none yet. Follows the Project out, like project_members\n project_id TEXT PRIMARY KEY REFERENCES projects(project_id) ON DELETE CASCADE,\n addresses TEXT NOT NULL -- JSON array of 'ssh:<alias>'\n);\nCREATE TABLE IF NOT EXISTS trace_files ( -- DERIVED CACHE of the on-disk Trace tree (services/trace-index.ts): the directories stay the single source of truth, every row is rebuildable from disk, and a row is never authority for absence — consumers reconcile + retry on a miss, so a stale index costs one extra scan, never a false 404\n project_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n session_id TEXT NOT NULL,\n file_index INTEGER NOT NULL, -- shard index NNN of <session_id>_NNN.jsonl (name/path are reconstructed from the row, never stored: the data root may move)\n date TEXT NOT NULL, -- date directory name (local yyyy-mm-dd)\n size_bytes INTEGER NOT NULL, -- last observed size (listings stat-refresh the returned page and write back; an actively-appended shard may lag in between)\n page_stats TEXT, -- cached message-window scan state at this shard's END (services/message-window.ts ShardPrefixRecord JSON); immutable shards only, NULL = not yet computed, nulled whenever size_bytes changes\n PRIMARY KEY (project_id, agent_id, session_id, file_index)\n);\nCREATE INDEX IF NOT EXISTS idx_trace_files_agent_date ON trace_files(project_id, agent_id, date);\nCREATE INDEX IF NOT EXISTS idx_trace_files_session ON trace_files(session_id);\nCREATE TABLE IF NOT EXISTS trace_sessions ( -- per-session facts read ONCE at registration from the earliest shard's head (same derived-cache rules as trace_files; listing needs zero head-reads)\n session_id TEXT PRIMARY KEY,\n project_id TEXT NOT NULL,\n agent_id TEXT NOT NULL,\n source TEXT, -- session_meta origin: 'subagent' | 'schedule' | NULL = user-created (or head not yet readable)\n workspace TEXT NOT NULL DEFAULT '',\n title TEXT, -- first-prompt fallback title (sessions.title always wins when present)\n provider TEXT, -- model reference from session_meta (CLI adoption reads it from here; NULL = meta unreadable / legacy without provider)\n model_id TEXT,\n first_ts TEXT, -- first record's timestamp (adoption's createdAt fallback when the id embeds no time)\n meta_read INTEGER NOT NULL DEFAULT 0 -- 1 once the head parsed; 0 = facts unknown, retried by the next reconcile that touches the session\n);\nCREATE INDEX IF NOT EXISTS idx_trace_sessions_agent ON trace_sessions(project_id, agent_id);\n`;\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/db/repos/users.ts","../src/db/repos/auth-sessions.ts"],"sourcesContent":["/**\n * users table repo: pure SQL wrapper, no business rules.\n * user_id is the login name (a semantic id, specified at creation, immutable).\n */\nimport type { DatabaseSync } from \"node:sqlite\";\n\nexport interface UserRow {\n userId: string;\n passwordHash: string;\n isAdmin: boolean;\n /** Still using the initial password (seeded / set by an admin); cleared to 0 once the user changes it. */\n passwordIsInitial: boolean;\n createdAt: string;\n}\n\nfunction mapRow(r: Record<string, unknown>): UserRow {\n return {\n userId: r.user_id as string,\n passwordHash: r.password_hash as string,\n isAdmin: (r.is_admin as number) === 1,\n passwordIsInitial: (r.password_is_initial as number) === 1,\n createdAt: r.created_at as string,\n };\n}\n\nexport class UsersRepo {\n constructor(private readonly db: DatabaseSync) {}\n\n insert(row: UserRow): void {\n this.db\n .prepare(\n \"INSERT INTO users (user_id, password_hash, is_admin, password_is_initial, created_at) VALUES (?, ?, ?, ?, ?)\",\n )\n .run(\n row.userId,\n row.passwordHash,\n row.isAdmin ? 1 : 0,\n row.passwordIsInitial ? 1 : 0,\n row.createdAt,\n );\n }\n\n findById(userId: string): UserRow | null {\n const r = this.db.prepare(\"SELECT * FROM users WHERE user_id = ?\").get(userId);\n return r ? mapRow(r) : null;\n }\n\n /** All users (for the admin user backend), ordered by creation time ascending. */\n list(): UserRow[] {\n const rows = this.db.prepare(\"SELECT * FROM users ORDER BY created_at ASC, user_id ASC\").all();\n return rows.map(mapRow);\n }\n\n count(): number {\n const r = this.db.prepare(\"SELECT COUNT(*) AS n FROM users\").get();\n return (r?.n as number) ?? 0;\n }\n\n /** Update the password hash; isInitial marks whether the password was set by someone else (seed / admin). */\n updatePassword(userId: string, passwordHash: string, isInitial: boolean): void {\n this.db\n .prepare(\"UPDATE users SET password_hash = ?, password_is_initial = ? WHERE user_id = ?\")\n .run(passwordHash, isInitial ? 1 : 0, userId);\n }\n\n /** Used by admin user deletion and account-creation compensation paths (owned Projects must be cleaned up first). */\n delete(userId: string): void {\n this.db.prepare(\"DELETE FROM users WHERE user_id = ?\").run(userId);\n }\n}\n","/**\n * auth_sessions table repo (server-side sessions backing the HttpOnly cookie). Stores only\n * sha256(token); the raw token appears only in the cookie. `issue()` is the ONE minting point,\n * so a CLI-minted row cannot drift from one the server issues itself.\n */\nimport { createHash, randomBytes } from \"node:crypto\";\nimport type { DatabaseSync } from \"node:sqlite\";\n\n/** How a session was established, as stored. NULL on rows formed before the column existed. */\nexport type SessionViaValue = \"password\" | \"desktop\" | \"setup\" | \"cli\";\n\nexport interface AuthSessionRow {\n tokenHash: string;\n userId: string;\n createdAt: string;\n expiresAt: string;\n via: SessionViaValue | null;\n}\n\n/** sha256 hex of a raw session token — the only form the database ever holds. */\nexport function sessionTokenHash(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n\nexport class AuthSessionsRepo {\n constructor(private readonly db: DatabaseSync) {}\n\n /**\n * Mints a session and returns the raw token for a cookie or the terminal. `maxTtlMs` caps a\n * caller-supplied lifetime (`--ttl-seconds`): one longer than an ordinary session would make\n * a leaked token file outlive every other credential, sliding forever. Expired rows are\n * swept here too, so a cron that only mints does not accumulate them.\n */\n issue(opts: {\n userId: string;\n via: SessionViaValue;\n now: Date;\n ttlMs: number;\n maxTtlMs?: number;\n }): { token: string; expiresAt: string } {\n const ttlMs = Math.max(1_000, Math.min(opts.ttlMs, opts.maxTtlMs ?? opts.ttlMs));\n const token = randomBytes(32).toString(\"base64url\");\n const createdAt = opts.now.toISOString();\n const expiresAt = new Date(opts.now.getTime() + ttlMs).toISOString();\n this.deleteExpired(createdAt);\n this.insert({\n tokenHash: sessionTokenHash(token),\n userId: opts.userId,\n createdAt,\n expiresAt,\n via: opts.via,\n });\n return { token, expiresAt };\n }\n\n insert(row: AuthSessionRow): void {\n this.db\n .prepare(\n \"INSERT INTO auth_sessions (token_hash, user_id, created_at, expires_at, via) VALUES (?, ?, ?, ?, ?)\",\n )\n .run(row.tokenHash, row.userId, row.createdAt, row.expiresAt, row.via);\n }\n\n findByTokenHash(tokenHash: string): AuthSessionRow | null {\n const r = this.db.prepare(\"SELECT * FROM auth_sessions WHERE token_hash = ?\").get(tokenHash);\n if (!r) return null;\n return {\n tokenHash: r.token_hash as string,\n userId: r.user_id as string,\n createdAt: r.created_at as string,\n expiresAt: r.expires_at as string,\n via: (r.via as SessionViaValue | null) ?? null,\n };\n }\n\n /** Sliding renewal: update the expiration time. */\n touch(tokenHash: string, expiresAt: string): void {\n this.db\n .prepare(\"UPDATE auth_sessions SET expires_at = ? WHERE token_hash = ?\")\n .run(expiresAt, tokenHash);\n }\n\n delete(tokenHash: string): void {\n this.db.prepare(\"DELETE FROM auth_sessions WHERE token_hash = ?\").run(tokenHash);\n }\n\n /** Opportunistically clean up expired sessions (called during login/validation). */\n deleteExpired(nowIso: string): void {\n this.db.prepare(\"DELETE FROM auth_sessions WHERE expires_at < ?\").run(nowIso);\n }\n\n /** Clear all sessions for a user (forces re-login after an admin resets the password). */\n deleteByUser(userId: string): void {\n this.db.prepare(\"DELETE FROM auth_sessions WHERE user_id = ?\").run(userId);\n }\n\n /**\n * Clear a user's sessions of one kind. Used to end EVERY first-login session when a password\n * is set: an earlier boot's link may still be live in someone's scrollback, and a `setup`\n * session may change the password without knowing the old one.\n */\n deleteByUserAndVia(userId: string, via: SessionViaValue): void {\n this.db.prepare(\"DELETE FROM auth_sessions WHERE user_id = ? AND via = ?\").run(userId, via);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auth/password.ts","../src/auth/service.ts","../src/auth/api-token.ts","../src/http/errors.ts"],"sourcesContent":["/**\n * Password hashing (slow-hash storage).\n *\n * Uses node:crypto's scrypt (built-in, no extra dependency, meets the same\n * slow-hash requirement as bcrypt/argon2). Storage format:\n * `scrypt$N$r$p$<salt b64>$<hash b64>` — parameters are stored alongside the hash,\n * so old hashes remain verifiable after future parameter tuning; comparison uses\n * timingSafeEqual to guard against timing side-channels.\n */\nimport { randomBytes, scrypt, timingSafeEqual } from \"node:crypto\";\n\n/**\n * Work factor for new hashes. `hashPassword` takes it as a defaulted argument so the server's\n * own test suite can hash at a token cost: almost every case there seeds an admin, provisions\n * users and logs them in, and one derivation at this strength costs on the order of 100ms.\n * Nothing on the production path passes the argument, and no configuration reaches it —\n * `buildAppDeps`, which carries the test override, is not part of this package's exports.\n */\nexport const SCRYPT_COST = 16384;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SALT_BYTES = 16;\nconst KEY_BYTES = 64;\n\nfunction scryptAsync(\n password: string,\n salt: Buffer,\n keyLen: number,\n n: number,\n r: number,\n p: number,\n): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n scrypt(password, salt, keyLen, { N: n, r, p, maxmem: 128 * 1024 * 1024 }, (err, key) => {\n if (err) reject(err);\n else resolve(key);\n });\n });\n}\n\n/**\n * Generates a password hash in `scrypt$N$r$p$salt$hash` format. `cost` is scrypt's N and\n * is recorded in the hash, so `verifyPassword` re-derives at whatever cost produced the\n * stored string.\n */\nexport async function hashPassword(password: string, cost: number = SCRYPT_COST): Promise<string> {\n const salt = randomBytes(SALT_BYTES);\n const key = await scryptAsync(password, salt, KEY_BYTES, cost, SCRYPT_R, SCRYPT_P);\n return [\n \"scrypt\",\n String(cost),\n String(SCRYPT_R),\n String(SCRYPT_P),\n salt.toString(\"base64\"),\n key.toString(\"base64\"),\n ].join(\"$\");\n}\n\n/** Verifies a password; returns false if the stored string has an invalid format (never throws, so the login path can uniformly treat it as a credential error). */\nexport async function verifyPassword(password: string, stored: string): Promise<boolean> {\n const parts = stored.split(\"$\");\n if (parts.length !== 6 || parts[0] !== \"scrypt\") return false;\n const n = Number(parts[1]);\n const r = Number(parts[2]);\n const p = Number(parts[3]);\n if (!Number.isInteger(n) || !Number.isInteger(r) || !Number.isInteger(p)) return false;\n let salt: Buffer;\n let expected: Buffer;\n try {\n salt = Buffer.from(parts[4]!, \"base64\");\n expected = Buffer.from(parts[5]!, \"base64\");\n } catch {\n return false;\n }\n if (salt.length === 0 || expected.length === 0) return false;\n let actual: Buffer;\n try {\n actual = await scryptAsync(password, salt, expected.length, n, r, p);\n } catch {\n return false;\n }\n return actual.length === expected.length && timingSafeEqual(actual, expected);\n}\n","/**\n * Auth service: admin seeding / login / logout / password change / session validation.\n *\n * No open registration: an empty users table seeds `admin` with a random password hashed and\n * discarded unseen, claimed through the first-login link; every other user is created by an\n * admin. Sessions are rows in auth_sessions (cookie holds the token, the row its sha256), so\n * they outlive a restart and renew in place.\n */\nimport { randomBytes } from \"node:crypto\";\nimport { tokensEqual } from \"./api-token.js\";\nimport type { UserInfo } from \"../api/types.js\";\nimport { HttpError } from \"../http/errors.js\";\nimport type { UserRow, UsersRepo } from \"../db/repos/users.js\";\nimport { sessionTokenHash } from \"../db/repos/auth-sessions.js\";\nimport type { SessionViaValue, AuthSessionsRepo } from \"../db/repos/auth-sessions.js\";\nimport type { AuthRuntimeState } from \"./runtime-state.js\";\nimport { SCRYPT_COST, hashPassword, verifyPassword } from \"./password.js\";\n\nexport const MIN_PASSWORD_LENGTH = 8;\n\n/** Built-in admin user_id. */\nexport const ADMIN_USER_ID = \"admin\";\n\n/**\n * Login throttling, per userId. After LOGIN_FREE_ATTEMPTS failures each attempt waits an\n * exponentially growing delay (1s doubling to a 60s cap), settling at a guess per minute.\n * Kept for nonexistent userIds too, so it is not an account-existence oracle. Deliberately NOT\n * covered: a concurrent burst before the first failure lands, and guesses spread across many\n * accounts.\n */\nconst LOGIN_FREE_ATTEMPTS = 5;\nconst LOGIN_BACKOFF_START_MS = 1000;\nconst LOGIN_BACKOFF_CAP_MS = 60_000;\n/** Failure entries idle longer than this are swept (bounds the map; far above the cap). */\nconst LOGIN_FAILURE_IDLE_MS = 15 * 60_000;\n\n/** Characters of base64url in a seeded password — 18 random bytes, 144 bits. */\nconst SEED_PASSWORD_CHARS = 24;\n\n/**\n * Never read or typed — hashed at once and discarded — which is what lets it be long enough\n * that the login endpoint, reachable the moment the account exists, has no search space.\n */\nexport function generateInitialAdminPassword(): string {\n return randomBytes(SEED_PASSWORD_CHARS).toString(\"base64url\").slice(0, SEED_PASSWORD_CHARS);\n}\n\n/**\n * \"desktop\" and \"setup\" may set a password without the old one (theirs is random and was never\n * shown); desktop-only routes open to \"desktop\" alone. \"token\" is the local API token's Bearer\n * header — per request, never a stored row. Anything else reads as \"password\".\n */\nexport type SessionVia = \"password\" | \"desktop\" | \"setup\" | \"token\";\n\nexport function toUserInfo(row: UserRow): UserInfo {\n return {\n userId: row.userId,\n isAdmin: row.isAdmin,\n passwordIsInitial: row.passwordIsInitial,\n createdAt: row.createdAt,\n };\n}\n\nexport interface AuthServiceDeps {\n users: UsersRepo;\n authSessions: AuthSessionsRepo;\n /**\n * Process-scoped auth values (runtime-state.ts). Held by the RUNTIME so the link a boot\n * printed survives a platform push; everything else about auth is rebuilt with the App.\n */\n state: AuthRuntimeState;\n /** Provisions the initial Project at signup (injected by project-service, to avoid a circular dependency). */\n provisionInitialProject: (user: UserRow, isAdmin: boolean) => Promise<void>;\n /** Fixed initial password for the seeded admin (config.seedAdminPassword); null generates a random one at seed time. */\n seedAdminPassword: string | null;\n sessionTtlMs: number;\n sessionRenewMs: number;\n /** Test double: scrypt work factor for hashes this service writes (SCRYPT_COST in production). */\n passwordHashCost?: number;\n now?: () => Date;\n}\n\nexport class AuthService {\n private readonly now: () => Date;\n private readonly hashCost: number;\n /** Session lifetime, for the cookie that must expire WITH the session, not before it. */\n get sessionTtlMs(): number {\n return this.deps.sessionTtlMs;\n }\n\n constructor(private readonly deps: AuthServiceDeps) {\n this.now = deps.now ?? (() => new Date());\n this.hashCost = deps.passwordHashCost ?? SCRYPT_COST;\n this.deps.authSessions.deleteExpired(this.now().toISOString());\n }\n\n /**\n * Null once the server is claimed, so the only setup session that exists is a printed one.\n * On demand rather than in the constructor: seedAdmin() runs after it, so \"claimed?\" has no\n * answer there. A cached link whose row is gone is re-minted, not handed out dead.\n */\n mintFirstLogin(): string | null {\n if (!this.adminPasswordIsInitial()) return null;\n if (\n this.deps.state.firstLoginToken !== null &&\n this.authenticateWithMeta(this.deps.state.firstLoginToken) === null\n ) {\n this.deps.state.firstLoginToken = null;\n }\n this.deps.state.firstLoginToken ??= this.issueSession(ADMIN_USER_ID, \"setup\");\n return this.deps.state.firstLoginToken;\n }\n\n /**\n * For the startup notice's pinned-seed gate (index.ts): a pin usually means the operator\n * knows the password, but an offline reset makes it stale, and the gate must see through\n * that or the rescue link stays suppressed.\n */\n async adminPasswordIs(password: string): Promise<boolean> {\n const row = this.deps.users.findById(ADMIN_USER_ID);\n return row !== null && (await verifyPassword(password, row.passwordHash));\n }\n\n /**\n * Startup seeding (idempotent): creates the built-in admin and adopts default_project when\n * the users table is empty; if the initial Project fails, the user row is rolled back and\n * the server retries on next startup.\n */\n async seedAdmin(): Promise<void> {\n if (this.deps.users.count() > 0) return;\n const password = this.deps.seedAdminPassword ?? generateInitialAdminPassword();\n // A pinned override must meet the same policy as every other password — rejected before\n // any insert, so a configuration typo cannot create a trivially weak privileged account.\n if (password.length < MIN_PASSWORD_LENGTH) {\n throw new Error(\n `PENGUIN_SEED_ADMIN_PASSWORD must be at least ${MIN_PASSWORD_LENGTH} characters.`,\n );\n }\n const user: UserRow = {\n userId: ADMIN_USER_ID,\n passwordHash: await hashPassword(password, this.hashCost),\n isAdmin: true,\n passwordIsInitial: true,\n createdAt: this.now().toISOString(),\n };\n this.deps.users.insert(user);\n try {\n await this.deps.provisionInitialProject(user, true);\n } catch (err) {\n this.deps.users.delete(user.userId);\n throw err;\n }\n }\n\n /**\n * Compared against the printed value, not merely verified: a cookie made out of ANY valid\n * token would let one person hand another a link into the sender's account. Not single-use,\n * because a prefetching browser would burn it before its reader clicked.\n */\n redeemFirstLogin(given: string): string | null {\n const expected = this.deps.state.firstLoginToken;\n if (expected === null || given === \"\" || !tokensEqual(given, expected)) return null;\n return this.authenticateWithMeta(expected) === null ? null : expected;\n }\n\n /** Whether the built-in admin still runs on its initial password (gates the first-login link). */\n adminPasswordIsInitial(): boolean {\n return this.deps.users.findById(ADMIN_USER_ID)?.passwordIsInitial === true;\n }\n\n /** Consecutive login failures per userId (see the throttling comment on the constants). */\n private readonly loginFailures = new Map<string, { failures: number; lastFailureAt: number }>();\n\n /** The wait imposed after `failures` consecutive failures (0 while within the free attempts). */\n private loginDelayMs(failures: number): number {\n const excess = failures - LOGIN_FREE_ATTEMPTS;\n if (excess <= 0) return 0;\n return Math.min(LOGIN_BACKOFF_START_MS * 2 ** (excess - 1), LOGIN_BACKOFF_CAP_MS);\n }\n\n async login(userId: string, password: string): Promise<{ user: UserInfo; token: string }> {\n const nowMs = this.now().getTime();\n for (const [key, entry] of this.loginFailures) {\n if (nowMs - entry.lastFailureAt > LOGIN_FAILURE_IDLE_MS) this.loginFailures.delete(key);\n }\n const failed = this.loginFailures.get(userId);\n if (failed) {\n const readyAt = failed.lastFailureAt + this.loginDelayMs(failed.failures);\n if (nowMs < readyAt) {\n throw new HttpError(\n 429,\n \"too_many_attempts\",\n `Too many failed sign-in attempts. Try again in ${Math.ceil((readyAt - nowMs) / 1000)}s.`,\n );\n }\n }\n const row = this.deps.users.findById(userId);\n const ok = row !== null && (await verifyPassword(password, row.passwordHash));\n if (!row || !ok) {\n this.loginFailures.set(userId, {\n failures: (failed?.failures ?? 0) + 1,\n lastFailureAt: this.now().getTime(),\n });\n throw new HttpError(401, \"invalid_credentials\", \"Incorrect username or password.\");\n }\n this.loginFailures.delete(userId);\n this.deps.authSessions.deleteExpired(this.now().toISOString());\n return { user: toUserInfo(row), token: this.issueSession(row.userId, \"password\") };\n }\n\n /**\n * Desktop-mode sign-in: an admin session with no password check — the claim route already\n * redeemed the shell's one-shot token, which is the credential here. Throws only on a\n * broken deployment (seeding runs before the route exists).\n */\n loginDesktop(): { user: UserInfo; token: string } {\n const row = this.deps.users.findById(ADMIN_USER_ID);\n if (!row) {\n throw new HttpError(500, \"internal\", \"Built-in admin has not been seeded.\");\n }\n return { user: toUserInfo(row), token: this.issueSession(row.userId, \"desktop\") };\n }\n\n /** Self password change (user settings): validates the old password first; the current session stays valid. */\n async changePassword(userId: string, oldPassword: string, newPassword: string): Promise<void> {\n const row = this.deps.users.findById(userId);\n if (!row || !(await verifyPassword(oldPassword, row.passwordHash))) {\n throw new HttpError(400, \"password_mismatch\", \"Current password is incorrect.\");\n }\n await this.setPassword(userId, newPassword);\n }\n\n /** No old-password check; the me route gates which sessions may call this. */\n async setInitialPassword(userId: string, newPassword: string): Promise<void> {\n await this.setPassword(userId, newPassword);\n }\n\n /**\n * Validation comes FIRST, so a rejected password leaves the first-login link alive: burning\n * it on a typo would strand the claimer until a restart.\n */\n private async setPassword(userId: string, newPassword: string): Promise<void> {\n if (newPassword.length < MIN_PASSWORD_LENGTH) {\n throw new HttpError(400, \"invalid_password\", \"Password must be at least 8 characters.\");\n }\n this.deps.users.updatePassword(userId, await hashPassword(newPassword, this.hashCost), false);\n // A successful set proves at least what the successful login that resets the counter\n // proves (the old password, or a desktop/setup session), so it resets it too. Without\n // this, anyone spamming the login endpoint holds the backoff window open, and the sign-in\n // that follows a first-login claim (routes/me.ts) would 429 AFTER the password committed —\n // reporting failure for a change that took, with the claimer's setup session already gone.\n this.loginFailures.delete(userId);\n // EVERY first-login session for this account, not just the link this process printed: an\n // earlier boot's link can still be live in a terminal scrollback, and a `setup` session is\n // allowed to change the password without knowing the old one — so one left behind is an\n // account takeover waiting to happen.\n if (userId === ADMIN_USER_ID) {\n this.deps.authSessions.deleteByUserAndVia(userId, \"setup\");\n this.deps.state.firstLoginToken = null;\n }\n }\n\n /** Ends a session: the row is the session, so logout deletes it. Unknown token is a no-op. */\n logout(token: string): void {\n this.deps.authSessions.delete(sessionTokenHash(token));\n }\n\n /**\n * The current boot's local API token (what control-env injection hands to tool\n * subprocesses); null when none was minted. It lives on the runtime state, not on this\n * service: the token was written to `<root>/api-token` by the process, so it must\n * outlive the App a push replaces (auth/runtime-state.ts).\n */\n localApiToken(): string | null {\n return this.deps.state.apiToken;\n }\n\n /**\n * Validates a Bearer token against the boot's local API token (constant-time compare):\n * a match authenticates as the built-in admin — holding the token proves filesystem\n * access to the data root, which is admin authority (see auth/api-token.ts). Returns\n * null on mismatch, when no token was minted, or when the admin row is missing.\n */\n authenticateApiToken(token: string): { user: UserRow; via: SessionVia } | null {\n const apiToken = this.deps.state.apiToken;\n if (apiToken === null || token.length === 0) return null;\n if (!tokensEqual(token, apiToken)) return null;\n const user = this.deps.users.findById(ADMIN_USER_ID);\n return user === null ? null : { user, via: \"token\" };\n }\n\n /**\n * Validates the cookie token. Sliding renewal tops the expiry up IN PLACE, so the cookie\n * value never changes and there is no second identity to revoke. Only a session whose own\n * span reaches the renewal window slides — a one-hour minted token must expire at its hour,\n * not stretch by being used.\n */\n authenticateWithMeta(token: string): { user: UserRow; via: SessionVia; renewed: boolean } | null {\n const tokenHash = sessionTokenHash(token);\n const session = this.deps.authSessions.findByTokenHash(tokenHash);\n if (!session) return null;\n const now = this.now();\n const expiresAt = Date.parse(session.expiresAt);\n if (expiresAt <= now.getTime()) {\n this.deps.authSessions.delete(tokenHash);\n return null;\n }\n const user = this.deps.users.findById(session.userId);\n if (!user) return null;\n const renewable = expiresAt - Date.parse(session.createdAt) >= this.deps.sessionRenewMs;\n let renewed = false;\n if (renewable && expiresAt - now.getTime() < this.deps.sessionRenewMs) {\n this.deps.authSessions.touch(\n tokenHash,\n new Date(now.getTime() + this.deps.sessionTtlMs).toISOString(),\n );\n renewed = true;\n }\n const via: SessionVia =\n session.via === \"desktop\" ? \"desktop\" : session.via === \"setup\" ? \"setup\" : \"password\";\n return { user, via, renewed };\n }\n\n private issueSession(userId: string, via: SessionViaValue): string {\n return this.deps.authSessions.issue({\n userId,\n via,\n now: this.now(),\n ttlMs: this.deps.sessionTtlMs,\n }).token;\n }\n}\n","/**\n * Local API token (`<root>/api-token`): the machine-local credential behind\n * `Authorization: Bearer`.\n *\n * Minted fresh at every server boot (same recipe as auth-session tokens) and written to\n * the data root with owner-only permissions, so anything that can read the file can call\n * the API as the admin. That equivalence is the authorization model, not an accident:\n * local filesystem access to the data root already IS admin authority — the same rule\n * `penguin server reset-admin-password` stands on (whoever can run it owns web.db\n * anyway) — and agents driving their own harness through the CLI is the product feature\n * this token exists for (server-driven Sessions hand it to tool subprocesses as\n * PENGUIN_API_TOKEN). Rotation per boot bounds the life of any leaked copy to the\n * process that minted it.\n */\nimport { createHash, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport function apiTokenPath(root: string): string {\n return path.join(root, \"api-token\");\n}\n\n/** Mints a boot-scoped local API token (the auth-session token recipe). */\nexport function mintApiToken(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/**\n * Persists the boot token (owner-only file, tmp + rename so a concurrent reader never\n * sees a partial write). Best-effort like the initial-password file: an exotic read-only\n * root must not stop the server — local CLI callers then fall back to\n * PENGUIN_API_TOKEN.\n */\nexport function storeApiToken(root: string, token: string): void {\n try {\n fs.mkdirSync(root, { recursive: true });\n const target = apiTokenPath(root);\n const tmp = `${target}.${process.pid}.tmp`;\n fs.writeFileSync(tmp, `${token}\\n`, { mode: 0o600 });\n fs.renameSync(tmp, target);\n } catch {\n // Best-effort: Bearer auth still works for callers holding the token some other way.\n }\n}\n\n/** The stored token, or null when absent/unreadable/empty (shared with the CLI's file fallback). */\nexport function readApiToken(root: string): string | null {\n try {\n const value = fs.readFileSync(apiTokenPath(root), \"utf8\").trim();\n return value === \"\" ? null : value;\n } catch {\n return null;\n }\n}\n\n/**\n * Constant-time equality for token checks: both sides are reduced to fixed-size sha256\n * digests first, so the comparison's timing depends on neither the content nor the\n * length of an attacker-supplied value.\n */\nexport function tokensEqual(a: string, b: string): boolean {\n const digest = (v: string): Buffer => createHash(\"sha256\").update(v).digest();\n return timingSafeEqual(digest(a), digest(b));\n}\n","/**\n * Unified HTTP error: `{error: {code, message}}` response body,\n * with message in English.\n *\n * Routes and services express business errors via throw HttpError; app-level onError\n * uniformly converges these into a JSON response, with unknown errors converged to 500\n * (never leaking internal details to the client).\n */\nimport type { Context } from \"hono\";\nimport type { ErrorBody } from \"../api/types.js\";\n\nexport class HttpError extends Error {\n constructor(\n readonly status: number,\n readonly code: string,\n message: string,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\nexport function errorBody(code: string, message: string): ErrorBody {\n return { error: { code, message } };\n}\n\n/**\n * Model missing a credential: the provider SDK throws this at **client-construction\n * time** (hit by both creating a Session and resuming a Session); the original message\n * is full of environment variable names, meaningless to a user — uniformly replaced\n * with a single actionable sentence. The frontend produces localized text by code\n * (message is only a fallback); see web's lib/api-error.ts.\n */\nexport function isMissingCredential(err: unknown): boolean {\n const message = err instanceof Error ? err.message : String(err);\n return /missing credentials|api[_ ]?key/i.test(message);\n}\n\nexport function modelCredentialMissing(modelId: string): HttpError {\n return new HttpError(\n 400,\n \"model_credential_missing\",\n `Model ${modelId} has no API key yet. Configure it on the Models page first.`,\n );\n}\n\n/** app.onError handler: maps HttpError through as-is; everything else is logged and converged to 500. */\nexport function handleError(err: Error, c: Context): Response {\n if (err instanceof HttpError) {\n return c.json(errorBody(err.code, err.message), err.status as 400);\n }\n // Unknown error: print the stack for diagnosis, but never expose details externally.\n console.error(`[server] Unhandled exception: ${err.stack ?? err.message}`);\n return c.json(errorBody(\"internal\", \"Internal server error.\"), 500);\n}\n"],"mappings":"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","names":["randomBytes","randomBytes","timingSafeEqual","timingSafeEqual","randomBytes"]}
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import { Resources } from '@prismshadow/penguin-core/kernel';
|
|
2
|
-
export { Disposable, Extension, ExtensionContext, ExtensionEvents, PenguinContext, PenguinInterface, ToolFactory, WorkflowFactory, WorkflowInstance, WorkflowInstances } from '@prismshadow/penguin-core/extension';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Screen snapshot -> ANSI restore stream, plus plain-text capture.
|
|
6
|
-
*
|
|
7
|
-
* This is what makes a browser refresh (or a reconnect from a second device) show the
|
|
8
|
-
* terminal exactly as it was: the daemon keeps an authoritative headless xterm per pty, and
|
|
9
|
-
* on attach it *re-renders* that grid as an escape-sequence stream the client's real xterm
|
|
10
|
-
* replays into an identical screen. That beats the two obvious alternatives:
|
|
11
|
-
*
|
|
12
|
-
* - Replaying the raw pty byte log: unbounded, and full-screen programs (vim, top) replay
|
|
13
|
-
* as a flicker of stale frames.
|
|
14
|
-
* - Sending a JSON cell grid: a 200k-object payload per attach, parsed on the UI thread.
|
|
15
|
-
*
|
|
16
|
-
* Cursor alignment is the subtle part. The restore writes `scrollback + rows` lines, so the
|
|
17
|
-
* client's viewport ends up holding exactly the server's viewport, and only then is the
|
|
18
|
-
* cursor placed with a viewport-relative CUP. Trailing blank rows are therefore written on
|
|
19
|
-
* purpose — dropping them would shift every subsequent row and land the cursor in the wrong
|
|
20
|
-
* place.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
interface CaptureLinesOptions {
|
|
24
|
-
/** Line index into scrollback+viewport; negative counts from the end (-1 = last line). */
|
|
25
|
-
start?: number;
|
|
26
|
-
end?: number;
|
|
27
|
-
}
|
|
28
|
-
interface CaptureLinesResult {
|
|
29
|
-
lines: string[];
|
|
30
|
-
totalLines: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface TerminalExitInfo {
|
|
34
|
-
exitCode: number;
|
|
35
|
-
signal: number | null;
|
|
36
|
-
lastOutputLines: string[];
|
|
37
|
-
}
|
|
38
|
-
interface CreateTerminalSessionOptions {
|
|
39
|
-
id?: string;
|
|
40
|
-
/** Absolute working directory (already resolved and validated by the manager). */
|
|
41
|
-
cwd: string;
|
|
42
|
-
/** Owning user id — a shell is the most privileged thing this server hands out. */
|
|
43
|
-
ownerUserId: string;
|
|
44
|
-
/**
|
|
45
|
-
* Stable per-user display number (tmux-style), assigned by the manager at creation. It
|
|
46
|
-
* travels with the terminal for its whole life — UI numbering must never depend on list
|
|
47
|
-
* position, which changes under user-reordered tabs.
|
|
48
|
-
*/
|
|
49
|
-
seq?: number;
|
|
50
|
-
name?: string;
|
|
51
|
-
shell?: string;
|
|
52
|
-
cols?: number;
|
|
53
|
-
rows?: number;
|
|
54
|
-
env?: Record<string, string>;
|
|
55
|
-
/**
|
|
56
|
-
* Where a pushed bundle's node-pty assets live (the hmr capability's assetsDir);
|
|
57
|
-
* absent falls back to the packaged require (see pty-module.ts).
|
|
58
|
-
*/
|
|
59
|
-
assets?: () => string | null;
|
|
60
|
-
}
|
|
61
|
-
interface TerminalSessionInfo {
|
|
62
|
-
id: string;
|
|
63
|
-
seq: number;
|
|
64
|
-
name: string;
|
|
65
|
-
cwd: string;
|
|
66
|
-
ownerUserId: string;
|
|
67
|
-
title: string | null;
|
|
68
|
-
cols: number;
|
|
69
|
-
rows: number;
|
|
70
|
-
pid: number;
|
|
71
|
-
createdAt: string;
|
|
72
|
-
alive: boolean;
|
|
73
|
-
exit: TerminalExitInfo | null;
|
|
74
|
-
}
|
|
75
|
-
type TerminalOutputListener = (data: string) => void;
|
|
76
|
-
type TerminalExitListener = (info: TerminalExitInfo) => void;
|
|
77
|
-
type TerminalTitleListener = (title: string | null) => void;
|
|
78
|
-
declare class TerminalSession {
|
|
79
|
-
readonly id: string;
|
|
80
|
-
readonly seq: number;
|
|
81
|
-
readonly cwd: string;
|
|
82
|
-
readonly ownerUserId: string;
|
|
83
|
-
readonly createdAt: Date;
|
|
84
|
-
private name;
|
|
85
|
-
private readonly terminal;
|
|
86
|
-
private readonly ptyProcess;
|
|
87
|
-
private readonly inputMode;
|
|
88
|
-
private readonly outputListeners;
|
|
89
|
-
private readonly exitListeners;
|
|
90
|
-
private readonly titleListeners;
|
|
91
|
-
private title;
|
|
92
|
-
private pendingTitle;
|
|
93
|
-
private titleTimer;
|
|
94
|
-
private sizeState;
|
|
95
|
-
private exitInfo;
|
|
96
|
-
private disposed;
|
|
97
|
-
constructor(options: CreateTerminalSessionOptions);
|
|
98
|
-
/** DA1 / DSR / OSC colour queries — see OSC_COLOR_QUERY_RESPONSES. */
|
|
99
|
-
private registerCapabilityResponders;
|
|
100
|
-
get alive(): boolean;
|
|
101
|
-
get exit(): TerminalExitInfo | null;
|
|
102
|
-
info(): TerminalSessionInfo;
|
|
103
|
-
rename(name: string): void;
|
|
104
|
-
write(data: string): void;
|
|
105
|
-
/**
|
|
106
|
-
* Applies a size request under the claim/update ownership rule, resizing both the pty and
|
|
107
|
-
* the daemon's emulator so snapshots keep matching what the shell laid out.
|
|
108
|
-
*/
|
|
109
|
-
resize(request: {
|
|
110
|
-
connectionId: string;
|
|
111
|
-
cols: number;
|
|
112
|
-
rows: number;
|
|
113
|
-
intent: "claim" | "update";
|
|
114
|
-
}): boolean;
|
|
115
|
-
releaseSize(connectionId: string): void;
|
|
116
|
-
get size(): {
|
|
117
|
-
cols: number;
|
|
118
|
-
rows: number;
|
|
119
|
-
};
|
|
120
|
-
/** The attach payload: the whole screen as an ANSI stream (see snapshot.ts). */
|
|
121
|
-
restoreStream(options?: {
|
|
122
|
-
scrollbackLines?: number;
|
|
123
|
-
}): string;
|
|
124
|
-
capture(options?: CaptureLinesOptions): CaptureLinesResult;
|
|
125
|
-
onOutput(listener: TerminalOutputListener): () => void;
|
|
126
|
-
onExit(listener: TerminalExitListener): () => void;
|
|
127
|
-
onTitleChange(listener: TerminalTitleListener): () => void;
|
|
128
|
-
kill(signal?: string): void;
|
|
129
|
-
dispose(): void;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
interface CreateTerminalRequest {
|
|
133
|
-
cwd: string;
|
|
134
|
-
ownerUserId: string;
|
|
135
|
-
name?: string;
|
|
136
|
-
cols?: number;
|
|
137
|
-
rows?: number;
|
|
138
|
-
shell?: string;
|
|
139
|
-
}
|
|
140
|
-
declare class TerminalManager {
|
|
141
|
-
private readonly resources;
|
|
142
|
-
private readonly opts;
|
|
143
|
-
/**
|
|
144
|
-
* The kernel's resource registry, threaded to every session it creates: a native module
|
|
145
|
-
* reaches the platform through it (pty-module.ts), and live ptys are registered in it so
|
|
146
|
-
* a hot swap hands them to the next platform instead of killing them.
|
|
147
|
-
*/
|
|
148
|
-
constructor(resources: Resources, opts?: {
|
|
149
|
-
graceMs?: number;
|
|
150
|
-
/** Where a pushed bundle's node-pty assets live (hmr.assetsDir); absent falls back to the packaged require. */
|
|
151
|
-
assets?: () => string | null;
|
|
152
|
-
});
|
|
153
|
-
private get graceMs();
|
|
154
|
-
private readonly sessions;
|
|
155
|
-
private readonly reapTimers;
|
|
156
|
-
/** Exit-listener unsubscribers, so quiesce() can detach this manager from delivered ptys. */
|
|
157
|
-
private readonly exitUnsubs;
|
|
158
|
-
/** Paired unregister per pty (register's return): the only way an entry leaves the registry. */
|
|
159
|
-
private readonly unregisters;
|
|
160
|
-
create(request: CreateTerminalRequest): Promise<TerminalSession>;
|
|
161
|
-
/**
|
|
162
|
-
* Takes a session into this manager AND into the runtime's resource registry. The
|
|
163
|
-
* registry is what makes a pty outlive a platform swap: it sits outside the reloadable
|
|
164
|
-
* tree, so the shell keeps running and the next instance claims it back (see
|
|
165
|
-
* hmr/resources.ts). The parked context carries only these ids.
|
|
166
|
-
*/
|
|
167
|
-
private track;
|
|
168
|
-
/**
|
|
169
|
-
* Reclaims the sessions a previous instance parked. An id the registry no longer knows
|
|
170
|
-
* is simply dropped: its shell died with the process that owned it, and a manager
|
|
171
|
-
* holding a handle to nothing would only fail later, further from the cause.
|
|
172
|
-
*/
|
|
173
|
-
adopt(ids: readonly string[]): void;
|
|
174
|
-
/**
|
|
175
|
-
* Park-side detach (hot swap only — never process exit): the ptys are DELIVERED to the
|
|
176
|
-
* next App through the registry, so this manager must stop acting on them. Exit
|
|
177
|
-
* listeners are unsubscribed (the successor adopts and re-listens), and every pending
|
|
178
|
-
* reap — each targeting an already-dead session that was therefore NOT parked for
|
|
179
|
-
* adoption — runs immediately instead of firing later from a dead generation, where it
|
|
180
|
-
* would release a registry id out from under whoever owns it next.
|
|
181
|
-
*/
|
|
182
|
-
quiesce(): void;
|
|
183
|
-
/** Handle ids for the parked context document — live sessions only. */
|
|
184
|
-
handleIds(): string[];
|
|
185
|
-
/** Looks up a session, enforcing ownership. Unknown and not-yours are both 404 — no probing. */
|
|
186
|
-
require(id: string, userId: string): TerminalSession;
|
|
187
|
-
get(id: string): TerminalSession | undefined;
|
|
188
|
-
list(userId: string): TerminalSession[];
|
|
189
|
-
listInfo(userId: string): TerminalSessionInfo[];
|
|
190
|
-
kill(id: string, userId: string): void;
|
|
191
|
-
/** Drops the session once nothing can usefully be read from it any more. */
|
|
192
|
-
private scheduleReap;
|
|
193
|
-
/** Server shutdown: every pty is a child process and must not outlive us. */
|
|
194
|
-
disposeAll(): void;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* The harness's extension surface — the `@prismshadow/penguin-server/extension` subpath.
|
|
199
|
-
*
|
|
200
|
-
* The contract itself lives in `@prismshadow/penguin-core/extension`; what this module adds
|
|
201
|
-
* is the harness's own members, contributed by AUGMENTING that contract rather than by
|
|
202
|
-
* redeclaring it. Core names nothing it cannot reach, this layer names what it owns, and
|
|
203
|
-
* an extension sees one `PenguinContext` either way.
|
|
204
|
-
*
|
|
205
|
-
* Re-exported here so an extension package has one import site for both halves.
|
|
206
|
-
*/
|
|
207
|
-
|
|
208
|
-
declare module "@prismshadow/penguin-core/extension" {
|
|
209
|
-
interface PenguinContext {
|
|
210
|
-
terminals: TerminalManager;
|
|
211
|
-
}
|
|
212
|
-
}
|