chore: replace em dashes with hyphens, add no-em-dash rule to CLAUDE.md
This commit is contained in:
1 parent
b36a696d0e
commit
6ee203f5e9
172 files changed
+860
-856
No files matched your search
@@ -19,7 +19,7 @@ import (
|
||||
// consoleConnect godoc
|
||||
//
|
||||
// @Summary Open a browser console session
|
||||
// @Description Mints a one-time session token for the /console/tunnel websocket. Requires a live agent — answers 409 agent_offline otherwise.
|
||||
// @Description Mints a one-time session token for the /console/tunnel websocket. Requires a live agent - answers 409 agent_offline otherwise.
|
||||
// @Tags console
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -109,7 +109,7 @@ func queryIntDefault(r *http.Request, key string, def int) int {
|
||||
// Every branch here logs. That is deliberate and worth keeping: this handler
|
||||
// spans four hops (session store, agent dispatch, relay announcement, guacd),
|
||||
// any of which can fail, and the client is told the same near-useless thing by
|
||||
// most of them — a 500 that guacamole then reports as an *upstream* error,
|
||||
// most of them - a 500 that guacamole then reports as an *upstream* error,
|
||||
// naming the wrong hop entirely. Without a line per branch the only evidence a
|
||||
// failure leaves is a GIN status code, and with several replicas you cannot
|
||||
// even tell which process produced it.
|
||||
@@ -215,7 +215,7 @@ func consoleTunnel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
// The client is deliberately told nothing specific, so this is the only
|
||||
// place the real reason exists — a failed dispatch and a relay that was
|
||||
// place the real reason exists - a failed dispatch and a relay that was
|
||||
// never announced are the same generic 500 to the browser.
|
||||
tlog("reject: open relay: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not open relay"})
|
||||
@@ -286,7 +286,7 @@ func consoleTunnel(c *gin.Context) {
|
||||
}
|
||||
|
||||
// The handshake is where guacd connects onward to the relay, so a
|
||||
// failure here is guacd reporting it could not reach %s:%d — the hop
|
||||
// failure here is guacd reporting it could not reach %s:%d - the hop
|
||||
// that has been hardest to see from either end.
|
||||
stream := guac.NewStream(conn, guac.SocketTimeout)
|
||||
if err := stream.Handshake(config); err != nil {
|
||||
|
||||
@@ -1631,14 +1631,14 @@
|
||||
"$ref": "#/components/schemas/models.AlertSettings"
|
||||
},
|
||||
"api_token_max_days": {
|
||||
"description": "APITokenMaxDays caps how long a newly created API token may live.\n\nA pointer for the same reason the retention fields are: absent must mean\nthe default, and the default here is no cap at all — never-expire tokens\nare allowed until an instance decides otherwise, so an upgrade changes\nnothing. Nil or 0 is no cap. A positive value refuses both a longer\nexpiry and a token with no expiry.\n\nIt is a policy on issuance, not on use: raising or lowering it never\ninvalidates a token that already exists.",
|
||||
"description": "APITokenMaxDays caps how long a newly created API token may live.\n\nA pointer for the same reason the retention fields are: absent must mean\nthe default, and the default here is no cap at all - never-expire tokens\nare allowed until an instance decides otherwise, so an upgrade changes\nnothing. Nil or 0 is no cap. A positive value refuses both a longer\nexpiry and a token with no expiry.\n\nIt is a policy on issuance, not on use: raising or lowering it never\ninvalidates a token that already exists.",
|
||||
"type": "integer"
|
||||
},
|
||||
"instance_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"local_login_enabled": {
|
||||
"description": "LocalLoginEnabled is a pointer because it is absent on every settings\ndocument written before this feature existed, and a plain bool would read\nabsent as disabled — turning off password login for the entire fleet at\nupgrade. Nil means enabled.",
|
||||
"description": "LocalLoginEnabled is a pointer because it is absent on every settings\ndocument written before this feature existed, and a plain bool would read\nabsent as disabled - turning off password login for the entire fleet at\nupgrade. Nil means enabled.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"secrets": {
|
||||
@@ -1970,7 +1970,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"fixed_in": {
|
||||
"description": "FixedIn empty means no vendor fix has been published. That is a real and\ncommon state and must never be conflated with \"not vulnerable\" — it is\nthe finding most in need of acceptance, since there is nothing to patch.",
|
||||
"description": "FixedIn empty means no vendor fix has been published. That is a real and\ncommon state and must never be conflated with \"not vulnerable\" - it is\nthe finding most in need of acceptance, since there is nothing to patch.",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
@@ -3319,7 +3319,7 @@
|
||||
},
|
||||
"/console/connect": {
|
||||
"post": {
|
||||
"description": "Mints a one-time session token for the /console/tunnel websocket. Requires a live agent — answers 409 agent_offline otherwise.",
|
||||
"description": "Mints a one-time session token for the /console/tunnel websocket. Requires a live agent - answers 409 agent_offline otherwise.",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
@@ -5645,7 +5645,7 @@
|
||||
},
|
||||
"/secrets/{group}/values": {
|
||||
"get": {
|
||||
"description": "Consumed by Kubernetes External Secrets Operator. Authenticated with a bearer token whose SHA-256 hash is stored in settings — a different credential from an API token, never substitutable for one.",
|
||||
"description": "Consumed by Kubernetes External Secrets Operator. Authenticated with a bearer token whose SHA-256 hash is stored in settings - a different credential from an API token, never substitutable for one.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Secret group name",
|
||||
@@ -6278,7 +6278,7 @@
|
||||
},
|
||||
"/servers/{id}/packages": {
|
||||
"get": {
|
||||
"description": "A server that has not reported yet answers reported=false rather than 404 — that is the normal state for the first hour after install.",
|
||||
"description": "A server that has not reported yet answers reported=false rather than 404 - that is the normal state for the first hour after install.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Server ID",
|
||||
@@ -8627,7 +8627,7 @@
|
||||
},
|
||||
"/vulnerabilities": {
|
||||
"get": {
|
||||
"description": "Groups findings by CVE, most severe first — the same CVE on forty servers is one decision, not forty rows.",
|
||||
"description": "Groups findings by CVE, most severe first - the same CVE on forty servers is one decision, not forty rows.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Filter by severity",
|
||||
@@ -8848,7 +8848,7 @@
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"description": "Requires a reason and a future expiry. Reopens automatically at expiry — permanent dismissal is never allowed.",
|
||||
"description": "Requires a reason and a future expiry. Reopens automatically at expiry - permanent dismissal is never allowed.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "Finding ID",
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
`),``,`You can apply a few solutions:`,['Add an `as="..."` prop, to ensure that we render an actual element instead of a "template".',`Render a single element as the child so that we can forward the props onto that element.`].map(e=>` - ${e}`).join(`
|
||||
`)].join(`
|
||||
`));let r=lae(e.props??{},o,c),a=ti(e,r,!0);for(let e in r)e.startsWith(`on`)&&(a.props||={},a.props[e]=r[e]);return a}return Array.isArray(s)&&s.length===1?s[0]:s}return ci(a,Object.assign({},o,c),{default:()=>s})}function cae(e){return e.flatMap(e=>e.type===H?cae(e.children):[e])}function lae(...e){if(e.length===0)return{};if(e.length===1)return e[0];let t={},n={};for(let r of e)for(let e in r)e.startsWith(`on`)&&typeof r[e]==`function`?(n[e]??(n[e]=[]),n[e].push(r[e])):t[e]=r[e];if(t.disabled||t[`aria-disabled`])return Object.assign(t,Object.fromEntries(Object.keys(n).map(e=>[e,void 0])));for(let e in n)Object.assign(t,{[e](t,...r){let i=n[e];for(let e of i){if(t instanceof Event&&t.defaultPrevented)return;e(t,...r)}}});return t}function uae(e){let t=Object.assign({},e);for(let e in t)t[e]===void 0&&delete t[e];return t}function Yo(e,t=[]){let n=Object.assign({},e);for(let e of t)e in n&&delete n[e];return n}function dae(e){return e==null?!1:typeof e.type==`string`||typeof e.type==`object`||typeof e.type==`function`}var Xo,fae,Zo=s((()=>{G(),Eo(),Xo=(e=>(e[e.None=0]=`None`,e[e.RenderStrategy=1]=`RenderStrategy`,e[e.Static=2]=`Static`,e))(Xo||{}),fae=(e=>(e[e.Unmount=0]=`Unmount`,e[e.Hidden=1]=`Hidden`,e))(fae||{})})),Qo,$o,es=s((()=>{G(),Zo(),Qo=(e=>(e[e.None=1]=`None`,e[e.Focusable=2]=`Focusable`,e[e.Hidden=4]=`Hidden`,e))(Qo||{}),$o=N({name:`Hidden`,props:{as:{type:[Object,String],default:`div`},features:{type:Number,default:1}},setup(e,{slots:t,attrs:n}){return()=>{let{features:r,...i}=e;return qo({ourProps:{"aria-hidden":(r&2)==2?!0:i[`aria-hidden`]??void 0,hidden:(r&4)==4||void 0,style:{position:`fixed`,top:1,left:1,width:1,height:0,padding:0,margin:-1,overflow:`hidden`,clip:`rect(0, 0, 0, 0)`,whiteSpace:`nowrap`,borderWidth:`0`,...(r&4)==4&&(r&2)!=2&&{display:`none`}}},theirProps:i,slot:{},attrs:n,slots:t,name:`Hidden`})}}})}));function ts(){return dr(rs,null)}function ns(e){ur(rs,e)}var rs,is,as=s((()=>{G(),rs=Symbol(`Context`),is=(e=>(e[e.Open=1]=`Open`,e[e.Closed=2]=`Closed`,e[e.Closing=4]=`Closing`,e[e.Opening=8]=`Opening`,e))(is||{})})),os,ss=s((()=>{os=(e=>(e.Space=` `,e.Enter=`Enter`,e.Escape=`Escape`,e.Backspace=`Backspace`,e.Delete=`Delete`,e.ArrowLeft=`ArrowLeft`,e.ArrowUp=`ArrowUp`,e.ArrowRight=`ArrowRight`,e.ArrowDown=`ArrowDown`,e.Home=`Home`,e.End=`End`,e.PageUp=`PageUp`,e.PageDown=`PageDown`,e.Tab=`Tab`,e))(os||{})}));function pae(e){function t(){document.readyState!==`loading`&&(e(),document.removeEventListener(`DOMContentLoaded`,t))}typeof window<`u`&&typeof document<`u`&&(document.addEventListener(`DOMContentLoaded`,t),t())}var mae=s((()=>{})),cs,hae=s((()=>{mae(),cs=[],pae(()=>{function e(e){e.target instanceof HTMLElement&&e.target!==document.body&&cs[0]!==e.target&&(cs.unshift(e.target),cs=cs.filter(e=>e!=null&&e.isConnected),cs.splice(10))}window.addEventListener(`click`,e,{capture:!0}),window.addEventListener(`mousedown`,e,{capture:!0}),window.addEventListener(`focus`,e,{capture:!0}),document.body.addEventListener(`click`,e,{capture:!0}),document.body.addEventListener(`mousedown`,e,{capture:!0}),document.body.addEventListener(`focus`,e,{capture:!0})})}));function gae(e){throw Error(`Unexpected object: `+e)}function _ae(e,t){let n=t.resolveItems();if(n.length<=0)return null;let r=t.resolveActiveIndex(),i=r??-1;switch(e.focus){case 0:for(let e=0;e<n.length;++e)if(!t.resolveDisabled(n[e],e,n))return e;return r;case 1:i===-1&&(i=n.length);for(let e=i-1;e>=0;--e)if(!t.resolveDisabled(n[e],e,n))return e;return r;case 2:for(let e=i+1;e<n.length;++e)if(!t.resolveDisabled(n[e],e,n))return e;return r;case 3:for(let e=n.length-1;e>=0;--e)if(!t.resolveDisabled(n[e],e,n))return e;return r;case 4:for(let r=0;r<n.length;++r)if(t.resolveId(n[r],r,n)===e.id)return r;return r;case 5:return null;default:gae(e)}}var ls,vae=s((()=>{ls=(e=>(e[e.First=0]=`First`,e[e.Previous=1]=`Previous`,e[e.Next=2]=`Next`,e[e.Last=3]=`Last`,e[e.Specific=4]=`Specific`,e[e.Nothing=5]=`Nothing`,e))(ls||{})}));function yae(e={},t=null,n=[]){for(let[r,i]of Object.entries(e))xae(n,bae(t,r),i);return n}function bae(e,t){return e?e+`[`+t+`]`:t}function xae(e,t,n){if(Array.isArray(n))for(let[r,i]of n.entries())xae(e,bae(t,r.toString()),i);else n instanceof Date?e.push([t,n.toISOString()]):typeof n==`boolean`?e.push([t,n?`1`:`0`]):typeof n==`string`?e.push([t,n]):typeof n==`number`?e.push([t,`${n}`]):n==null?e.push([t,``]):yae(n,t,e)}var Sae=s((()=>{})),Cae=s((()=>{}));function us(e,t,n,r){Oo.isServer||fr(i=>{e??=window,e.addEventListener(t,n,r),i(()=>e.removeEventListener(t,n,r))})}var ds=s((()=>{G(),ko()}));function fs(){let e=A(0);return eae(`keydown`,t=>{t.key===`Tab`&&(e.value=+!!t.shiftKey)}),e}var ps,wae=s((()=>{G(),tae(),ps=(e=>(e[e.Forwards=0]=`Forwards`,e[e.Backwards=1]=`Backwards`,e))(ps||{})}));function Tae(e){if(!e)return new Set;if(typeof e==`function`)return new Set(e());let t=new Set;for(let n of e.value){let e=Co(n);e instanceof HTMLElement&&t.add(e)}return t}function Eae(e){let t=A(cs.slice());return pr(
Line truncated
|
||||
%c💡 Tip:%c Pass a plain object instead — wrapping a proxy inside another proxy may cause weird bugs.
|
||||
%c💡 Tip:%c Pass a plain object instead - wrapping a proxy inside another proxy may cause weird bugs.
|
||||
%c🔍 Debug Info:%c The problematic value is shown below:`,`background: #fdd835; color: #000; font-weight: bold; padding: 2px 4px; border-radius: 3px;`,`color: inherit;`,i,`color: #00bfa5; font-weight: bold;`,`color: inherit;`,`color: #03a9f4; font-weight: bold;`,`color: inherit;`,r,e),console.groupCollapsed(`%c📜 Proxy assignment trace`,`color: #9c27b0; font-weight: bold;`),console.trace({value:r,raw:a}),console.groupEnd()),Object.entries(a).forEach(([e,t])=>{let r=n(t,i+1);Reflect.set(a,e,r)||console.warn(`%c🚫 Readonly Property Error:%c Failed to set property "%s" on object.
|
||||
%c💡 Tip:%c This property is readonly or non-configurable. You cannot unpack a readonly property — the value was not updated.
|
||||
%c💡 Tip:%c This property is readonly or non-configurable. You cannot unpack a readonly property - the value was not updated.
|
||||
%c🔍 Debug Info:%c Property: %s | Value: %o | Object: %o`,`background: #f44336; color: #fff; font-weight: bold; padding: 2px 4px; border-radius: 3px;`,`color: inherit;`,e,`color: #00bfa5; font-weight: bold;`,`color: inherit;`,`color: #03a9f4; font-weight: bold;`,`color: inherit;`,e,r,a)}),a)};return n(e)}})),wd,Td=s((()=>{ed(),Zu(),Cd(),wd=e=>{let t=new WeakSet,n=new WeakMap,r=e=>{if(!Qu(e)&&!Array.isArray(e))return e;let i=Sd(e,{depth:1});if(n.has(i))return n.get(i);if(t.has(i))return`[circular]`;if(t.add(i),`$ref`in e){let t=K(e),a=r(t),o=Object.entries(e).filter(([e])=>e!==`$ref`&&e!==`$ref-value`);if(o.length>0&&Qu(a)){let e={...a};for(let[t,n]of o)e[t]=r(n);return n.set(i,e),e}return n.set(i,a),a}if(Array.isArray(e)){let t=e.map(r);return n.set(i,t),t}let a=Object.fromEntries(Object.entries(e).map(([e,t])=>[e,r(t)]));return n.set(i,a),a};return r(e)}})),Ed,Dd,Od=s((()=>{Zu(),Ed=(e,t,n)=>{if(!e&&!t)return;let r=!!e&&Object.keys(e).length>0,i=n||Object.keys(e??{})[0]||``,a=K(e?.[i]);if(a!==void 0)return a;if((!r||!n)&&t!==void 0)return{value:K(t)}},Dd=(e,t,n)=>{if(`content`in e){let r=e.content?.[n??Object.keys(e.content)[0]??``],i=Ed(r?.examples,r?.example,t);if(i!==void 0)return i}if(`examples`in e||`example`in e){let n=Ed(e.examples,e.example,t);if(n!==void 0)return n}let r=K(e);if(`schema`in r&&r.schema){let e=r.schema;if(`default`in e&&e.default!==void 0)return{value:e.default};if(`enum`in e&&e.enum?.[0]!==void 0)return{value:e.enum[0]};if(`examples`in e&&e.examples?.[0]!==void 0)return{value:e.examples[0]};if(`example`in e&&e.example!==void 0)return{value:e.example}}}})),kd,Kve,Ad,qve,jd,Md,Nd=s((()=>{Zu(),Cd(),kd=e=>typeof e==`object`&&!!e&&`$dynamicRef`in e&&typeof e.$dynamicRef==`string`,Kve=e=>`$dynamicAnchor`in e||`$id`in e||`$defs`in e,Ad=new WeakMap,qve=e=>{let t=Sd(e,{depth:1}),n=Ad.get(t);if(n)return n;let r=new Map,i=e=>{if(!e||typeof e!=`object`)return;let t=e.$dynamicAnchor;typeof t==`string`&&!r.has(t)&&r.set(t,K(e,Xu))};i(e);let a=e.$defs;if(a&&typeof a==`object`)for(let e of Object.keys(a))i(a[e]);return Ad.set(t,r),r},jd=(e,t)=>Kve(t)?[...e,t]:e,Md=(e,t)=>{let n=e.startsWith(`#`)?e.slice(1):void 0;if(!(!n||n.startsWith(`/`)))for(let e of t){let t=qve(e).get(n);if(t)return t}}}));function Jve(e){return Rd(e)&&!Pd(e)&&!Bd(e)&&Symbol.asyncIterator in e}function Pd(e){return Array.isArray(e)}function Yve(e){return typeof e==`bigint`}function Fd(e){return typeof e==`boolean`}function Id(e){return e instanceof globalThis.Date}function Xve(e){return typeof e==`function`}function Zve(e){return Rd(e)&&!Pd(e)&&!Bd(e)&&Symbol.iterator in e}function Qve(e){return e===null}function Ld(e){return typeof e==`number`}function Rd(e){return typeof e==`object`&&!!e}function $ve(e){return e instanceof globalThis.RegExp}function zd(e){return typeof e==`string`}function eye(e){return typeof e==`symbol`}function Bd(e){return e instanceof globalThis.Uint8Array}function Vd(e){return e===void 0}var Hd=s((()=>{}));function tye(e){return e.map(e=>Ud(e))}function nye(e){return new Date(e.getTime())}function rye(e){return new Uint8Array(e)}function iye(e){return new RegExp(e.source,e.flags)}function aye(e){let t={};for(let n of Object.getOwnPropertyNames(e))t[n]=Ud(e[n]);for(let n of Object.getOwnPropertySymbols(e))t[n]=Ud(e[n]);return t}function Ud(e){return Pd(e)?tye(e):Id(e)?nye(e):Bd(e)?rye(e):$ve(e)?iye(e):Rd(e)?aye(e):e}function Wd(e){return Ud(e)}var Gd=s((()=>{Hd()}));function Kd(e,t){return Wd(t===void 0?e:{...t,...e})}var qd=s((()=>{Gd()})),oye=s((()=>{qd(),Gd()}));function sye(e){return Zd(e)&&globalThis.Symbol.asyncIterator in e}function cye(e){return Zd(e)&&globalThis.Symbol.iterator in e}function lye(e){return e instanceof globalThis.Promise}function Jd(e){return e instanceof Date&&globalThis.Number.isFinite(e.getTime())}function uye(e){return e instanceof globalThis.Map}function dye(e){return e instanceof globalThis.Set}function fye(e){return globalThis.ArrayBuffer.isView(e)}function Yd(e){return e instanceof globalThis.Uint8Array}function Xd(e,t){return t in e}function Zd(e){return typeof e==`object`&&!!e}function Qd(e){return globalThis.Array.isArray(e)&&!globalThis.ArrayBuffer.isView(e)}function $d(e){return e===void 0}function ef(e){return e===null}function tf(e){return typeof e==`boolean`}function nf(e){return typeof e==`number`}function pye(e){return globalThis.Number.isInteger(e)}function rf(e){return typeof e==`bigint`}function af(e){return typeof e==`string`}function of(e){return typeof e==`function`}function sf(e){return typeof e==`symbol`}function mye(e){return rf(e)||tf(e)||ef(e)||nf(e)||af(e)||sf(e)||$d(e)}var cf=s((()=>{})),lf=s((()=>{cf()})),uf,hye=s((()=>{lf(),(function(e){e.InstanceMode=`default`,e.ExactOptionalPropertyTypes=!1,e.AllowArrayObject=!1,e.AllowNaN=!1,e.AllowNullVoid=!1;function t(t,n){return e.ExactOptionalPropertyTypes?n in t:t[n]!==void 0}e.IsExactOptionalProperty=t;function n(t){let n=Zd(t);return e.AllowArrayObject?n:n&&!Qd(t)}e.IsObjectLike=n;function r(e){retu
Line truncated
|
||||
|
||||
- The browser URL must be on the same domain as the server URL.
|
||||
@@ -66,7 +66,7 @@
|
||||
https://github.com/highlightjs/highlight.js/issues/2277`),i=e,r=t),n===void 0&&(n=!0);let a={code:r,language:i};pe(`before:highlight`,a);let o=a.result?a.result:v(a.language,a.code,n);return o.code=a.code,pe(`after:highlight`,o),o}function v(e,n,i,a){let c=Object.create(null);function u(e,t){return e.keywords[t]}function d(){if(!C.keywords){ce.addText(ue);return}let e=0;C.keywordPatternRe.lastIndex=0;let t=C.keywordPatternRe.exec(ue),n=``;for(;t;){n+=ue.substring(e,t.index);let r=ae.case_insensitive?t[0].toLowerCase():t[0],i=u(C,r);if(i){let[e,a]=i;if(ce.addText(n),n=``,c[r]=(c[r]||0)+1,c[r]<=Ue&&(de+=a),e.startsWith(`_`))n+=t[0];else{let n=ae.classNameAliases[e]||e;m(t[0],n)}}else n+=t[0];e=C.keywordPatternRe.lastIndex,t=C.keywordPatternRe.exec(ue)}n+=ue.substring(e),ce.addText(n)}function f(){if(ue===``)return;let e=null;if(typeof C.subLanguage==`string`){if(!t[C.subLanguage]){ce.addText(ue);return}e=v(C.subLanguage,ue,!0,se[C.subLanguage]),se[C.subLanguage]=e._top}else e=te(ue,C.subLanguage.length?C.subLanguage:null);C.relevance>0&&(de+=e.relevance),ce.__addSublanguage(e._emitter,e.language)}function p(){C.subLanguage==null?d():f(),ue=``}function m(e,t){e!==``&&(ce.startScope(t),ce.addText(e),ce.endScope())}function h(e,t){let n=1,r=t.length-1;for(;n<=r;){if(!e._emit[n]){n++;continue}let r=ae.classNameAliases[e[n]]||e[n],i=t[n];r?m(i,r):(ue=i,d(),ue=``),n++}}function g(e,t){return e.scope&&typeof e.scope==`string`&&ce.openNode(ae.classNameAliases[e.scope]||e.scope),e.beginScope&&(e.beginScope._wrap?(m(ue,ae.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),ue=``):e.beginScope._multi&&(h(e.beginScope,t),ue=``)),C=Object.create(e,{parent:{value:C}}),C}function _(e,t,n){let i=ee(e.endRe,n);if(i){if(e[`on:end`]){let n=new r(e);e[`on:end`](t,n),n.isMatchIgnored&&(i=!1)}if(i){for(;e.endsParent&&e.parent;)e=e.parent;return e}}if(e.endsWithParent)return _(e.parent,t,n)}function y(e){return C.matcher.regexIndex===0?(ue+=e[0],1):(fe=!0,0)}function b(e){let t=e[0],n=e.rule,i=new r(n),a=[n.__beforeBegin,n[`on:begin`]];for(let n of a)if(n&&(n(e,i),i.isMatchIgnored))return y(t);return n.skip?ue+=t:(n.excludeBegin&&(ue+=t),p(),!n.returnBegin&&!n.excludeBegin&&(ue=t)),g(n,e),n.returnBegin?0:t.length}function ne(e){let t=e[0],r=n.substring(e.index),i=_(C,e,r);if(!i)return He;let a=C;C.endScope&&C.endScope._wrap?(p(),m(t,C.endScope._wrap)):C.endScope&&C.endScope._multi?(p(),h(C.endScope,e)):a.skip?ue+=t:(a.returnEnd||a.excludeEnd||(ue+=t),p(),a.excludeEnd&&(ue=t));do C.scope&&ce.closeNode(),!C.skip&&!C.subLanguage&&(de+=C.relevance),C=C.parent;while(C!==i.parent);return i.starts&&g(i.starts,e),a.returnEnd?0:t.length}function x(){let e=[];for(let t=C;t!==ae;t=t.parent)t.scope&&e.unshift(t.scope);e.forEach(e=>ce.openNode(e))}let re={};function ie(t,r){let a=r&&r[0];if(ue+=t,a==null)return p(),0;if(re.type===`begin`&&r.type===`end`&&re.index===r.index&&a===``){if(ue+=n.slice(r.index,r.index+1),!o){let t=Error(`0 width match regex (${e})`);throw t.languageName=e,t.badRule=re.rule,t}return 1}if(re=r,r.type===`begin`)return b(r);if(r.type===`illegal`&&!i){let e=Error(`Illegal lexeme "`+a+`" for mode "`+(C.scope||`<unnamed>`)+`"`);throw e.mode=C,e}else if(r.type===`end`){let e=ne(r);if(e!==He)return e}if(r.type===`illegal`&&a===``)return ue+=`
|
||||
`,1;if(T>1e5&&T>r.index*3)throw Error(`potential infinite loop, way more iterations than matches`);return ue+=a,a.length}let ae=le(e);if(!ae)throw Ee(s.replace(`{}`,e)),Error(`Unknown language: "`+e+`"`);let oe=Fe(ae),S=``,C=a||oe,se={},ce=new l.__emitter(l);x();let ue=``,de=0,w=0,T=0,fe=!1;try{if(ae.__emitTokens)ae.__emitTokens(n,ce);else{for(C.matcher.considerAll();;){T++,fe?fe=!1:C.matcher.considerAll(),C.matcher.lastIndex=w;let e=C.matcher.exec(n);if(!e)break;let t=ie(n.substring(w,e.index),e);w=e.index+t}ie(n.substring(w))}return ce.finalize(),S=ce.toHTML(),{language:e,value:S,relevance:de,illegal:!1,_emitter:ce,_top:C}}catch(t){if(t.message&&t.message.includes(`Illegal`))return{language:e,value:Be(n),illegal:!0,relevance:0,_illegalBy:{message:t.message,index:w,context:n.slice(w-100,w+100),mode:t.mode,resultSoFar:S},_emitter:ce};if(o)return{language:e,value:Be(n),illegal:!1,relevance:0,errorRaised:t,_emitter:ce,_top:C};throw t}}function b(e){let t={value:Be(e),illegal:!1,relevance:0,_top:c,_emitter:new l.__emitter(l)};return t._emitter.addText(e),t}function te(e,n){n=n||l.languages||Object.keys(t);let r=b(e),i=n.filter(le).filter(de).map(t=>v(t,e,!1));i.unshift(r);let[a,o]=i.sort((e,t)=>{if(e.relevance!==t.relevance)return t.relevance-e.relevance;if(e.language&&t.language){if(le(e.language).supersetOf===t.language)return 1;if(le(t.language).supersetOf===e.language)return-1}return 0}),s=a;return s.secondBest=o,s}function ne(e,t,n){let r=t&&i[t]||n;e.classList.add(`hljs`),e.classList.add(`language-${r}`)}function x(e){let t=null,n=d(e);if(u(n))return;if(pe(`before:highlightElement`,{el:e,language:n}),e.dataset.highlighted){console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e);return}if(e.children.length>0&&(l.ignoreUnescapedHTML||(console.warn(`One of your code blocks includes unescaped HTML. This is a potentially serious security risk.`),console.warn(`https://github.com/highlightjs/highlight.js/wiki/security`),console.warn(`The element with unescaped HTML:`),console.warn(e)),l.throwUnescapedHTML))throw new ze(`One of your code blocks includes unescaped HTML.`,e.innerHTML);t=e;let r=t.textContent,i=n?p(r,{language:n,ignoreIllegals:!0}):te(r);e.innerHTML=i.value,e.dataset.highlighted=`yes`,ne(e,n,i.language),e.result={language:i.language,re:i.relevance,relevance:i.relevance},i.secondBest&&(e.secondBest={language:i.secondBest.language,relevance:i.secondBest.relevance}),pe(`after:highlightElement`,{el:e,result:i,text:r})}function re(e){l=Ve(l,e)}let ie=()=>{S(),Oe(`10.6.0`,`initHighlighting() deprecated. Use highlightAll() now.`)};function ae(){S(),Oe(`10.6.0`,`initHighlightingOnLoad() deprecated. Use highlightAll() now.`)}let oe=!1;function S(){function e(){S()}if(document.readyState===`loading`){oe||window.addEventListener(`DOMContentLoaded`,e,!1),oe=!0;return}document.querySelectorAll(l.cssSelector).forEach(x)}function C(n,r){let i=null;try{i=r(e)}catch(e){if(Ee(`Language definition for '{}' could not be registered.`.replace(`{}`,n)),o)Ee(e);else throw e;i=c}i.name||=n,t[n]=i,i.rawDefinition=r.bind(null,e),i.aliases&&ue(i.aliases,{languageName:n})}function se(e){delete t[e];for(let t of Object.keys(i))i[t]===e&&delete i[t]}function ce(){return Object.keys(t)}function le(e){return e=(e||``).toLowerCase(),t[e]||t[i[e]]}function ue(e,{languageName:t}){typeof e==`string`&&(e=[e]),e.forEach(e=>{i[e.toLowerCase()]=t})}function de(e){let t=le(e);return t&&!t.disableAutodetect}function w(e){e[`before:highlightBlock`]&&!e[`before:highlightElement`]&&(e[`before:highlightElement`]=t=>{e[`before:highlightBlock`](Object.assign({block:t.el},t))}),e[`after:highlightBlock`]&&!e[`after:highlightElement`]&&(e[`after:highlightElement`]=t=>{e[`after:highlightBlock`](Object.assign({block:t.el},t))})}function T(e){w(e),a.push(e)}function fe(e){let t=a.indexOf(e);t!==-1&&a.splice(t,1)}function pe(e,t){let n=e;a.forEach(function(e){e[n]&&e[n](t)})}function D(e){return Oe(`10.7.0`,`highlightBlock will be removed entirely in v12.0`),Oe(`10.7.0`,`Please use highlightElement now.`),x(e)}Object.assign(e,{highlight:p,highlightAuto:te,highlightAll:S,highlightElement:x,highlightBlock:D,configure:re,initHighlighting:ie,initHighlightingOnLoad:ae,registerLanguage:C,unregisterLanguage:se,listLanguages:ce,getLanguage:le,registerAliases:ue,autoDetection:de,inherit:Ve,addPlugin:T,removePlugin:fe}),e.debugMode=function(){o=!1},e.safeMode=function(){o=!0},e.versionString=Re,e.regex={concat:_,lookahead:m,either:y,optional:g,anyNumberOfTimes:h};for(let e in E)typeof E[e]==`object`&&n(E[e]);return Object.assign(e,E),e},Ge=We({});Ge.newInstance=()=>We({}),t.exports=Ge,Ge.HighlightJS=Ge,Ge.default=Ge}))()).default,UR={},nit=`hljs-`,rit=class{constructor(e){this.options=e,this.root={type:`root`,children:[],data:{language:void 0,relevance:0}},this.stack=[this.root]}addText(e){if(e===``)return;let t=this.stack[this.stack.length-1],n=t.children[t.children.length-1];n&&n.type===`text`?n.value+=e:t.chil
Line truncated
|
||||
`},l]}}function Fit(e){let t=gat(e),n=Sat,r=`([\\w-]+|@\\{[\\w-]+\\})`,i=[],a=[],o=function(e){return{className:`string`,begin:`~?`+e+`.*?`+e}},s=function(e,t,n){return{className:e,begin:t,relevance:n}},c={$pattern:/[a-z-]+/,keyword:`and or not only`,attribute:bat.join(` `)},l={begin:`\\(`,end:`\\)`,contains:a,keywords:c,relevance:0};a.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,o(`'`),o(`"`),t.CSS_NUMBER_MODE,{begin:`(url|data-uri)\\(`,starts:{className:`string`,end:`[\\)\\n]`,excludeEnd:!0}},t.HEXCOLOR,l,s(`variable`,`@@?[\\w-]+`,10),s(`variable`,`@\\{[\\w-]+\\}`),s(`built_in`,"~?`[^`]*?`"),{className:`attribute`,begin:`[\\w-]+\\s*:`,end:`:`,returnBegin:!0,excludeEnd:!0},t.IMPORTANT,{beginKeywords:`and not`},t.FUNCTION_DISPATCH);let u=a.concat({begin:/\{/,end:/\}/,contains:i}),d={beginKeywords:`when`,endsWithParent:!0,contains:[{beginKeywords:`and not`}].concat(a)},f={begin:`([\\w-]+|@\\{[\\w-]+\\})\\s*:`,returnBegin:!0,end:/[;}]/,relevance:0,contains:[{begin:/-(webkit|moz|ms|o)-/},t.CSS_VARIABLE,{className:`attribute`,begin:`\\b(`+xat.join(`|`)+`)\\b`,end:/(?=:)/,starts:{endsWithParent:!0,illegal:`[<=$]`,relevance:0,contains:a}}]},p={className:`keyword`,begin:`@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b`,starts:{end:`[;{}]`,keywords:c,returnEnd:!0,contains:a,relevance:0}},m={className:`variable`,variants:[{begin:`@[\\w-]+\\s*:`,relevance:15},{begin:`@[\\w-]+`}],starts:{end:`[;}]`,returnEnd:!0,contains:u}},h={variants:[{begin:`[\\.#:&\\[>]`,end:`[;{}]`},{begin:r,end:/\{/}],returnBegin:!0,returnEnd:!0,illegal:`[<='$"]`,relevance:0,contains:[e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,d,s(`keyword`,`all\\b`),s(`variable`,`@\\{[\\w-]+\\}`),{begin:`\\b(`+yat.join(`|`)+`)\\b`,className:`selector-tag`},t.CSS_NUMBER_MODE,s(`selector-tag`,r,0),s(`selector-id`,`#([\\w-]+|@\\{[\\w-]+\\})`),s(`selector-class`,`\\.([\\w-]+|@\\{[\\w-]+\\})`,0),s(`selector-tag`,`&`,0),t.ATTRIBUTE_SELECTOR_MODE,{className:`selector-pseudo`,begin:`:(`+cz.join(`|`)+`)`},{className:`selector-pseudo`,begin:`:(:)?(`+lz.join(`|`)+`)`},{begin:/\(/,end:/\)/,relevance:0,contains:u},{begin:`!important`},t.FUNCTION_DISPATCH]},g={begin:`[\\w-]+:(:)?(${n.join(`|`)})`,returnBegin:!0,contains:[h]};return i.push(e.C_LINE_COMMENT_MODE,e.C_BLOCK_COMMENT_MODE,p,m,g,f,h,d,t.FUNCTION_DISPATCH),{name:`Less`,case_insensitive:!0,illegal:`[=>'/<($"]`,contains:i}}function Iit(e){let t=`\\[=*\\[`,n=`\\]=*\\]`,r={begin:t,end:n,contains:[`self`]},i=[e.COMMENT(`--(?!\\[=*\\[)`,`$`),e.COMMENT(`--\\[=*\\[`,n,{contains:[r],relevance:10})];return{name:`Lua`,aliases:[`pluto`],keywords:{$pattern:e.UNDERSCORE_IDENT_RE,literal:`true false nil`,keyword:`and break do else elseif end for goto if in local not or repeat return then until while`,built_in:`_G _ENV _VERSION __index __newindex __mode __call __metatable __tostring __len __gc __add __sub __mul __div __mod __pow __concat __unm __eq __lt __le assert collectgarbage dofile error getfenv getmetatable ipairs load loadfile loadstring module next pairs pcall print rawequal rawget rawset require select setfenv setmetatable tonumber tostring type unpack xpcall arg self coroutine resume yield status wrap create running debug getupvalue debug sethook getmetatable gethook setmetatable setlocal traceback setfenv getinfo setupvalue getlocal getregistry getfenv io lines write close flush open output type read stderr stdin input stdout popen tmpfile math log max acos huge ldexp pi cos tanh pow deg tan cosh sinh random randomseed frexp ceil floor rad abs sqrt modf asin min mod fmod log10 atan2 exp sin atan os exit setlocale date getenv difftime remove time clock tmpname rename execute package preload loadlib loaded loaders cpath config path seeall string sub upper len gfind rep find match char dump gmatch reverse byte format gsub lower table setn insert getn foreachi maxn foreach concat sort remove`},contains:i.concat([{className:`function`,beginKeywords:`function`,end:`\\)`,contains:[e.inherit(e.TITLE_MODE,{begin:`([_a-zA-Z]\\w*\\.)*([_a-zA-Z]\\w*:)?[_a-zA-Z]\\w*`}),{className:`params`,begin:`\\(`,endsWithParent:!0,contains:i}].concat(i)},e.C_NUMBER_MODE,e.APOS_STRING_MODE,e.QUOTE_STRING_MODE,{className:`string`,begin:t,end:n,contains:[r],relevance:5}])}}function Lit(e){let t={className:`variable`,variants:[{begin:`\\$\\(`+e.UNDERSCORE_IDENT_RE+`\\)`,contains:[e.BACKSLASH_ESCAPE]},{begin:/\$[@%<?\^\+\*]/}]},n={className:`string`,begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,t]},r={className:`variable`,begin:/\$\([\w-]+\s/,end:/\)/,keywords:{built_in:`subst patsubst strip findstring filter filter-out sort word wordlist firstword lastword dir notdir suffix basename addsuffix addprefix join wildcard realpath abspath error warning shell origin flavor foreach if or and call eval file value`},contains:[t,n]},i={begin:`^`+e.UNDERSCORE_IDENT_RE+`\\s*(?=[:+?]?=)`},a={className:`meta`,begin:/^\.PHONY:/,end:/$/,keywords:{$pattern:/[\.\w]+/,keyword:`.PHONY`}},o={className:`sectLine truncated
|
||||
]`,m={scope:`string`,variants:[u,l,d,f]},h={scope:`number`,variants:[{begin:`\\b0[bB][01]+(?:_[01]+)*\\b`},{begin:`\\b0[oO][0-7]+(?:_[0-7]+)*\\b`},{begin:`\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b`},{begin:`(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?`}],relevance:0},g=[`false`,`null`,`true`],_=`__CLASS__.__DIR__.__FILE__.__FUNCTION__.__COMPILER_HALT_OFFSET__.__LINE__.__METHOD__.__NAMESPACE__.__TRAIT__.die.echo.exit.include.include_once.print.require.require_once.array.abstract.and.as.binary.bool.boolean.break.callable.case.catch.class.clone.const.continue.declare.default.do.double.else.elseif.empty.enddeclare.endfor.endforeach.endif.endswitch.endwhile.enum.eval.extends.final.finally.float.for.foreach.from.global.goto.if.implements.instanceof.insteadof.int.integer.interface.isset.iterable.list.match|0.mixed.new.never.object.or.private.protected.public.readonly.real.return.string.switch.throw.trait.try.unset.use.var.void.while.xor.yield`.split(`.`),v=`Error|0.AppendIterator.ArgumentCountError.ArithmeticError.ArrayIterator.ArrayObject.AssertionError.BadFunctionCallException.BadMethodCallException.CachingIterator.CallbackFilterIterator.CompileError.Countable.DirectoryIterator.DivisionByZeroError.DomainException.EmptyIterator.ErrorException.Exception.FilesystemIterator.FilterIterator.GlobIterator.InfiniteIterator.InvalidArgumentException.IteratorIterator.LengthException.LimitIterator.LogicException.MultipleIterator.NoRewindIterator.OutOfBoundsException.OutOfRangeException.OuterIterator.OverflowException.ParentIterator.ParseError.RangeException.RecursiveArrayIterator.RecursiveCachingIterator.RecursiveCallbackFilterIterator.RecursiveDirectoryIterator.RecursiveFilterIterator.RecursiveIterator.RecursiveIteratorIterator.RecursiveRegexIterator.RecursiveTreeIterator.RegexIterator.RuntimeException.SeekableIterator.SplDoublyLinkedList.SplFileInfo.SplFileObject.SplFixedArray.SplHeap.SplMaxHeap.SplMinHeap.SplObjectStorage.SplObserver.SplPriorityQueue.SplQueue.SplStack.SplSubject.SplTempFileObject.TypeError.UnderflowException.UnexpectedValueException.UnhandledMatchError.ArrayAccess.BackedEnum.Closure.Fiber.Generator.Iterator.IteratorAggregate.Serializable.Stringable.Throwable.Traversable.UnitEnum.WeakReference.WeakMap.Directory.__PHP_Incomplete_Class.parent.php_user_filter.self.static.stdClass`.split(`.`),y={keyword:_,literal:(e=>{let t=[];return e.forEach(e=>{t.push(e),e.toLowerCase()===e?t.push(e.toUpperCase()):t.push(e.toLowerCase())}),t})(g),built_in:v},b=e=>e.map(e=>e.replace(/\|\d+$/,``)),ee={variants:[{match:[/new/,t.concat(p,`+`),t.concat(`(?!`,b(v).join(`\\b|`),`\\b)`),i],scope:{1:`keyword`,4:`title.class`}}]},te=t.concat(r,`\\b(?!\\()`),ne={variants:[{match:[t.concat(/::/,t.lookahead(/(?!class\b)/)),te],scope:{2:`variable.constant`}},{match:[/::/,/class/],scope:{2:`variable.language`}},{match:[i,t.concat(/::/,t.lookahead(/(?!class\b)/)),te],scope:{1:`title.class`,3:`variable.constant`}},{match:[i,t.concat(`::`,t.lookahead(/(?!class\b)/))],scope:{1:`title.class`}},{match:[i,/::/,/class/],scope:{1:`title.class`,3:`variable.language`}}]},x={scope:`attr`,match:t.concat(r,t.lookahead(`:`),t.lookahead(/(?!::)/))},re={relevance:0,begin:/\(/,end:/\)/,keywords:y,contains:[x,o,ne,e.C_BLOCK_COMMENT_MODE,m,h,ee]},ie={relevance:0,match:[/\b/,t.concat(`(?!fn\\b|function\\b|`,b(_).join(`\\b|`),`|`,b(v).join(`\\b|`),`\\b)`),r,t.concat(p,`*`),t.lookahead(/(?=\()/)],scope:{3:`title.function.invoke`},contains:[re]};re.contains.push(ie);let ae=[x,ne,e.C_BLOCK_COMMENT_MODE,m,h,ee],oe={begin:t.concat(/#\[\s*\\?/,t.either(i,a)),beginScope:`meta`,end:/]/,endScope:`meta`,keywords:{literal:g,keyword:[`new`,`array`]},contains:[{begin:/\[/,end:/]/,keywords:{literal:g,keyword:[`new`,`array`]},contains:[`self`,...ae]},...ae,{scope:`meta`,variants:[{match:i},{match:a}]}]};return{case_insensitive:!1,keywords:y,contains:[oe,e.HASH_COMMENT_MODE,e.COMMENT(`//`,`$`),e.COMMENT(`/\\*`,`\\*/`,{contains:[{scope:`doctag`,match:`@[A-Za-z]+`}]}),{match:/__halt_compiler\(\);/,keywords:`__halt_compiler`,starts:{scope:`comment`,end:e.MATCH_NOTHING_RE,contains:[{match:/\?>/,scope:`meta`,endsParent:!0}]}},s,{scope:`variable.language`,match:/\$this\b/},o,ie,ne,{match:[/const/,/\s/,r],scope:{1:`keyword`,3:`variable.constant`}},ee,{scope:`function`,relevance:0,beginKeywords:`fn function`,end:/[;{]/,excludeEnd:!0,illegal:`[$%\\[]`,contains:[{beginKeywords:`use`},e.UNDERSCORE_TITLE_MODE,{begin:`=>`,endsParent:!0},{scope:`params`,begin:`\\(`,end:`\\)`,excludeBegin:!0,excludeEnd:!0,keywords:y,contains:[`self`,oe,o,ne,e.C_BLOCK_COMMENT_MODE,m,h]}]},{scope:`class`,variants:[{beginKeywords:`enum`,illegal:/[($"]/},{beginKeywords:`class interface trait`,illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{beginKeywords:`extends implements`},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:`namespace`,relevance:0,end:`;`,illegal:/[.']/,contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:`title.class`})]},{beginKeywords:`use`,relevance:0,en
Line truncated
|
||||
]`,m={scope:`string`,variants:[u,l,d,f]},h={scope:`number`,variants:[{begin:`\\b0[bB][01]+(?:_[01]+)*\\b`},{begin:`\\b0[oO][0-7]+(?:_[0-7]+)*\\b`},{begin:`\\b0[xX][\\da-fA-F]+(?:_[\\da-fA-F]+)*\\b`},{begin:`(?:\\b\\d+(?:_\\d+)*(\\.(?:\\d+(?:_\\d+)*))?|\\B\\.\\d+)(?:[eE][+-]?\\d+)?`}],relevance:0},g=[`false`,`null`,`true`],_=`__CLASS__.__DIR__.__FILE__.__FUNCTION__.__COMPILER_HALT_OFFSET__.__LINE__.__METHOD__.__NAMESPACE__.__TRAIT__.die.echo.exit.include.include_once.print.require.require_once.array.abstract.and.as.binary.bool.boolean.break.callable.case.catch.class.clone.const.continue.declare.default.do.double.else.elseif.empty.enddeclare.endfor.endforeach.endif.endswitch.endwhile.enum.eval.extends.final.finally.float.for.foreach.from.global.goto.if.implements.instanceof.insteadof.int.integer.interface.isset.iterable.list.match|0.mixed.new.never.object.or.private.protected.public.readonly.real.return.string.switch.throw.trait.try.unset.use.var.void.while.xor.yield`.split(`.`),v=`Error|0.AppendIterator.ArgumentCountError.ArithmeticError.ArrayIterator.ArrayObject.AssertionError.BadFunctionCallException.BadMethodCallException.CachingIterator.CallbackFilterIterator.CompileError.Countable.DirectoryIterator.DivisionByZeroError.DomainException.EmptyIterator.ErrorException.Exception.FilesystemIterator.FilterIterator.GlobIterator.InfiniteIterator.InvalidArgumentException.IteratorIterator.LengthException.LimitIterator.LogicException.MultipleIterator.NoRewindIterator.OutOfBoundsException.OutOfRangeException.OuterIterator.OverflowException.ParentIterator.ParseError.RangeException.RecursiveArrayIterator.RecursiveCachingIterator.RecursiveCallbackFilterIterator.RecursiveDirectoryIterator.RecursiveFilterIterator.RecursiveIterator.RecursiveIteratorIterator.RecursiveRegexIterator.RecursiveTreeIterator.RegexIterator.RuntimeException.SeekableIterator.SplDoublyLinkedList.SplFileInfo.SplFileObject.SplFixedArray.SplHeap.SplMaxHeap.SplMinHeap.SplObjectStorage.SplObserver.SplPriorityQueue.SplQueue.SplStack.SplSubject.SplTempFileObject.TypeError.UnderflowException.UnexpectedValueException.UnhandledMatchError.ArrayAccess.BackedEnum.Closure.Fiber.Generator.Iterator.IteratorAggregate.Serializable.Stringable.Throwable.Traversable.UnitEnum.WeakReference.WeakMap.Directory.__PHP_Incomplete_Class.parent.php_user_filter.self.static.stdClass`.split(`.`),y={keyword:_,literal:(e=>{let t=[];return e.forEach(e=>{t.push(e),e.toLowerCase()===e?t.push(e.toUpperCase()):t.push(e.toLowerCase())}),t})(g),built_in:v},b=e=>e.map(e=>e.replace(/\|\d+$/,``)),ee={variants:[{match:[/new/,t.concat(p,`+`),t.concat(`(?!`,b(v).join(`\\b|`),`\\b)`),i],scope:{1:`keyword`,4:`title.class`}}]},te=t.concat(r,`\\b(?!\\()`),ne={variants:[{match:[t.concat(/::/,t.lookahead(/(?!class\b)/)),te],scope:{2:`variable.constant`}},{match:[/::/,/class/],scope:{2:`variable.language`}},{match:[i,t.concat(/::/,t.lookahead(/(?!class\b)/)),te],scope:{1:`title.class`,3:`variable.constant`}},{match:[i,t.concat(`::`,t.lookahead(/(?!class\b)/))],scope:{1:`title.class`}},{match:[i,/::/,/class/],scope:{1:`title.class`,3:`variable.language`}}]},x={scope:`attr`,match:t.concat(r,t.lookahead(`:`),t.lookahead(/(?!::)/))},re={relevance:0,begin:/\(/,end:/\)/,keywords:y,contains:[x,o,ne,e.C_BLOCK_COMMENT_MODE,m,h,ee]},ie={relevance:0,match:[/\b/,t.concat(`(?!fn\\b|function\\b|`,b(_).join(`\\b|`),`|`,b(v).join(`\\b|`),`\\b)`),r,t.concat(p,`*`),t.lookahead(/(?=\()/)],scope:{3:`title.function.invoke`},contains:[re]};re.contains.push(ie);let ae=[x,ne,e.C_BLOCK_COMMENT_MODE,m,h,ee],oe={begin:t.concat(/#\[\s*\\?/,t.either(i,a)),beginScope:`meta`,end:/]/,endScope:`meta`,keywords:{literal:g,keyword:[`new`,`array`]},contains:[{begin:/\[/,end:/]/,keywords:{literal:g,keyword:[`new`,`array`]},contains:[`self`,...ae]},...ae,{scope:`meta`,variants:[{match:i},{match:a}]}]};return{case_insensitive:!1,keywords:y,contains:[oe,e.HASH_COMMENT_MODE,e.COMMENT(`//`,`$`),e.COMMENT(`/\\*`,`\\*/`,{contains:[{scope:`doctag`,match:`@[A-Za-z]+`}]}),{match:/__halt_compiler\(\);/,keywords:`__halt_compiler`,starts:{scope:`comment`,end:e.MATCH_NOTHING_RE,contains:[{match:/\?>/,scope:`meta`,endsParent:!0}]}},s,{scope:`variable.language`,match:/\$this\b/},o,ie,ne,{match:[/const/,/\s/,r],scope:{1:`keyword`,3:`variable.constant`}},ee,{scope:`function`,relevance:0,beginKeywords:`fn function`,end:/[;{]/,excludeEnd:!0,illegal:`[$%\\[]`,contains:[{beginKeywords:`use`},e.UNDERSCORE_TITLE_MODE,{begin:`=>`,endsParent:!0},{scope:`params`,begin:`\\(`,end:`\\)`,excludeBegin:!0,excludeEnd:!0,keywords:y,contains:[`self`,oe,o,ne,e.C_BLOCK_COMMENT_MODE,m,h]}]},{scope:`class`,variants:[{beginKeywords:`enum`,illegal:/[($"]/},{beginKeywords:`class interface trait`,illegal:/[:($"]/}],relevance:0,end:/\{/,excludeEnd:!0,contains:[{beginKeywords:`extends implements`},e.UNDERSCORE_TITLE_MODE]},{beginKeywords:`namespace`,relevance:0,end:`;`,illegal:/[.']/,contains:[e.inherit(e.UNDERSCORE_TITLE_MODE,{scope:`title.class`})]},{beginKeywords:`use`,relevance:0,en
Line truncated
|
||||
\f\r &/=>`.split(``),`
|
||||
\f\r "&'/=>\``.split(``)],[`\0
|
||||
\f\r "&'/<=>`.split(``),`\0
|
||||
@@ -128,7 +128,7 @@ https://github.com/highlightjs/highlight.js/issues/2277`),i=e,r=t),n===void 0&&(
|
||||
])`},{atBreak:!0,character:`-`,after:`(?:[ \r
|
||||
-])`},{atBreak:!0,before:`\\d+`,character:`.`,after:`(?:[ \r
|
||||
]|$)`},{atBreak:!0,character:`<`,after:`[!/?A-Za-z]`},{character:`<`,after:`[!/?A-Za-z]`,inConstruct:`phrasing`,notInConstruct:cU},{character:`<`,inConstruct:`destinationLiteral`},{atBreak:!0,character:`=`},{atBreak:!0,character:`>`},{character:`>`,inConstruct:`destinationLiteral`},{atBreak:!0,character:`[`},{character:`[`,inConstruct:`phrasing`,notInConstruct:cU},{character:`[`,inConstruct:[`label`,`reference`]},{character:`\\`,after:`[\\r\\n]`,inConstruct:`phrasing`},{character:`]`,inConstruct:[`label`,`reference`]},{atBreak:!0,character:`_`},{character:`_`,inConstruct:`phrasing`,notInConstruct:cU},{atBreak:!0,character:"`"},{character:"`",inConstruct:[`codeFencedLangGraveAccent`,`codeFencedMetaGraveAccent`]},{character:"`",inConstruct:`phrasing`,notInConstruct:cU},{atBreak:!0,character:`~`}],lU={AElig:`Æ`,AMP:`&`,Aacute:`Á`,Abreve:`Ă`,Acirc:`Â`,Acy:`А`,Afr:`𝔄`,Agrave:`À`,Alpha:`Α`,Amacr:`Ā`,And:`⩓`,Aogon:`Ą`,Aopf:`𝔸`,ApplyFunction:``,Aring:`Å`,Ascr:`𝒜`,Assign:`≔`,Atilde:`Ã`,Auml:`Ä`,Backslash:`∖`,Barv:`⫧`,Barwed:`⌆`,Bcy:`Б`,Because:`∵`,Bernoullis:`ℬ`,Beta:`Β`,Bfr:`𝔅`,Bopf:`𝔹`,Breve:`˘`,Bscr:`ℬ`,Bumpeq:`≎`,CHcy:`Ч`,COPY:`©`,Cacute:`Ć`,Cap:`⋒`,CapitalDifferentialD:`ⅅ`,Cayleys:`ℭ`,Ccaron:`Č`,Ccedil:`Ç`,Ccirc:`Ĉ`,Cconint:`∰`,Cdot:`Ċ`,Cedilla:`¸`,CenterDot:`·`,Cfr:`ℭ`,Chi:`Χ`,CircleDot:`⊙`,CircleMinus:`⊖`,CirclePlus:`⊕`,CircleTimes:`⊗`,ClockwiseContourIntegral:`∲`,CloseCurlyDoubleQuote:`”`,CloseCurlyQuote:`’`,Colon:`∷`,Colone:`⩴`,Congruent:`≡`,Conint:`∯`,ContourIntegral:`∮`,Copf:`ℂ`,Coproduct:`∐`,CounterClockwiseContourIntegral:`∳`,Cross:`⨯`,Cscr:`𝒞`,Cup:`⋓`,CupCap:`≍`,DD:`ⅅ`,DDotrahd:`⤑`,DJcy:`Ђ`,DScy:`Ѕ`,DZcy:`Џ`,Dagger:`‡`,Darr:`↡`,Dashv:`⫤`,Dcaron:`Ď`,Dcy:`Д`,Del:`∇`,Delta:`Δ`,Dfr:`𝔇`,DiacriticalAcute:`´`,DiacriticalDot:`˙`,DiacriticalDoubleAcute:`˝`,DiacriticalGrave:"`",DiacriticalTilde:`˜`,Diamond:`⋄`,DifferentialD:`ⅆ`,Dopf:`𝔻`,Dot:`¨`,DotDot:`⃜`,DotEqual:`≐`,DoubleContourIntegral:`∯`,DoubleDot:`¨`,DoubleDownArrow:`⇓`,DoubleLeftArrow:`⇐`,DoubleLeftRightArrow:`⇔`,DoubleLeftTee:`⫤`,DoubleLongLeftArrow:`⟸`,DoubleLongLeftRightArrow:`⟺`,DoubleLongRightArrow:`⟹`,DoubleRightArrow:`⇒`,DoubleRightTee:`⊨`,DoubleUpArrow:`⇑`,DoubleUpDownArrow:`⇕`,DoubleVerticalBar:`∥`,DownArrow:`↓`,DownArrowBar:`⤓`,DownArrowUpArrow:`⇵`,DownBreve:`̑`,DownLeftRightVector:`⥐`,DownLeftTeeVector:`⥞`,DownLeftVector:`↽`,DownLeftVectorBar:`⥖`,DownRightTeeVector:`⥟`,DownRightVector:`⇁`,DownRightVectorBar:`⥗`,DownTee:`⊤`,DownTeeArrow:`↧`,Downarrow:`⇓`,Dscr:`𝒟`,Dstrok:`Đ`,ENG:`Ŋ`,ETH:`Ð`,Eacute:`É`,Ecaron:`Ě`,Ecirc:`Ê`,Ecy:`Э`,Edot:`Ė`,Efr:`𝔈`,Egrave:`È`,Element:`∈`,Emacr:`Ē`,EmptySmallSquare:`◻`,EmptyVerySmallSquare:`▫`,Eogon:`Ę`,Eopf:`𝔼`,Epsilon:`Ε`,Equal:`⩵`,EqualTilde:`≂`,Equilibrium:`⇌`,Escr:`ℰ`,Esim:`⩳`,Eta:`Η`,Euml:`Ë`,Exists:`∃`,ExponentialE:`ⅇ`,Fcy:`Ф`,Ffr:`𝔉`,FilledSmallSquare:`◼`,FilledVerySmallSquare:`▪`,Fopf:`𝔽`,ForAll:`∀`,Fouriertrf:`ℱ`,Fscr:`ℱ`,GJcy:`Ѓ`,GT:`>`,Gamma:`Γ`,Gammad:`Ϝ`,Gbreve:`Ğ`,Gcedil:`Ģ`,Gcirc:`Ĝ`,Gcy:`Г`,Gdot:`Ġ`,Gfr:`𝔊`,Gg:`⋙`,Gopf:`𝔾`,GreaterEqual:`≥`,GreaterEqualLess:`⋛`,GreaterFullEqual:`≧`,GreaterGreater:`⪢`,GreaterLess:`≷`,GreaterSlantEqual:`⩾`,GreaterTilde:`≳`,Gscr:`𝒢`,Gt:`≫`,HARDcy:`Ъ`,Hacek:`ˇ`,Hat:`^`,Hcirc:`Ĥ`,Hfr:`ℌ`,HilbertSpace:`ℋ`,Hopf:`ℍ`,HorizontalLine:`─`,Hscr:`ℋ`,Hstrok:`Ħ`,HumpDownHump:`≎`,HumpEqual:`≏`,IEcy:`Е`,IJlig:`IJ`,IOcy:`Ё`,Iacute:`Í`,Icirc:`Î`,Icy:`И`,Idot:`İ`,Ifr:`ℑ`,Igrave:`Ì`,Im:`ℑ`,Imacr:`Ī`,ImaginaryI:`ⅈ`,Implies:`⇒`,Int:`∬`,Integral:`∫`,Intersection:`⋂`,InvisibleComma:``,InvisibleTimes:``,Iogon:`Į`,Iopf:`𝕀`,Iota:`Ι`,Iscr:`ℐ`,Itilde:`Ĩ`,Iukcy:`І`,Iuml:`Ï`,Jcirc:`Ĵ`,Jcy:`Й`,Jfr:`𝔍`,Jopf:`𝕁`,Jscr:`𝒥`,Jsercy:`Ј`,Jukcy:`Є`,KHcy:`Х`,KJcy:`Ќ`,Kappa:`Κ`,Kcedil:`Ķ`,Kcy:`К`,Kfr:`𝔎`,Kopf:`𝕂`,Kscr:`𝒦`,LJcy:`Љ`,LT:`<`,Lacute:`Ĺ`,Lambda:`Λ`,Lang:`⟪`,Laplacetrf:`ℒ`,Larr:`↞`,Lcaron:`Ľ`,Lcedil:`Ļ`,Lcy:`Л`,LeftAngleBracket:`⟨`,LeftArrow:`←`,LeftArrowBar:`⇤`,LeftArrowRightArrow:`⇆`,LeftCeiling:`⌈`,LeftDoubleBracket:`⟦`,LeftDownTeeVector:`⥡`,LeftDownVector:`⇃`,LeftDownVectorBar:`⥙`,LeftFloor:`⌊`,LeftRightArrow:`↔`,LeftRightVector:`⥎`,LeftTee:`⊣`,LeftTeeArrow:`↤`,LeftTeeVector:`⥚`,LeftTriangle:`⊲`,LeftTriangleBar:`⧏`,LeftTriangleEqual:`⊴`,LeftUpDownVector:`⥑`,LeftUpTeeVector:`⥠`,LeftUpVector:`↿`,LeftUpVectorBar:`⥘`,LeftVector:`↼`,LeftVectorBar:`⥒`,Leftarrow:`⇐`,Leftrightarrow:`⇔`,LessEqualGreater:`⋚`,LessFullEqual:`≦`,LessGreater:`≶`,LessLess:`⪡`,LessSlantEqual:`⩽`,LessTilde:`≲`,Lfr:`𝔏`,Ll:`⋘`,Lleftarrow:`⇚`,Lmidot:`Ŀ`,LongLeftArrow:`⟵`,LongLeftRightArrow:`⟷`,LongRightArrow:`⟶`,Longleftarrow:`⟸`,Longleftrightarrow:`⟺`,Longrightarrow:Line truncated
|
||||
`,Nfr:`𝔑`,NoBreak:``,NonBreakingSpace:`\xA0`,Nopf:`ℕ`,Not:`⫬`,NotCongruent:`≢`,NotCupCap:`≭`,NotDoubleVerticalBar:`∦`,NotElement:`∉`,NotEqual:`≠`,NotEqualTilde:`≂̸`,NotExists:`∄`,NotGreater:`≯`,NotGreaterEqual:`≱`,NotGreaterFullEqual:`≧̸`,NotGreaterGreater:`≫̸`,NotGreaterLess:`≹`,NotGreaterSlantEqual:`⩾̸`,NotGreaterTilde:`≵`,NotHumpDownHump:`≎̸`,NotHumpEqual:`≏̸`,NotLeftTriangle:`⋪`,NotLeftTriangleBar:`⧏̸`,NotLeftTriangleEqual:`⋬`,NotLess:`≮`,NotLessEqual:`≰`,NotLessGreater:`≸`,NotLessLess:`≪̸`,NotLessSlantEqual:`⩽̸`,NotLessTilde:`≴`,NotNestedGreaterGreater:`⪢̸`,NotNestedLessLess:`⪡̸`,NotPrecedes:`⊀`,NotPrecedesEqual:`⪯̸`,NotPrecedesSlantEqual:`⋠`,NotReverseElement:`∌`,NotRightTriangle:`⋫`,NotRightTriangleBar:`⧐̸`,NotRightTriangleEqual:`⋭`,NotSquareSubset:`⊏̸`,NotSquareSubsetEqual:`⋢`,NotSquareSuperset:`⊐̸`,NotSquareSupersetEqual:`⋣`,NotSubset:`⊂⃒`,NotSubsetEqual:`⊈`,NotSucceeds:`⊁`,NotSucceedsEqual:`⪰̸`,NotSucceedsSlantEqual:`⋡`,NotSucceedsTilde:`≿̸`,NotSuperset:`⊃⃒`,NotSupersetEqual:`⊉`,NotTilde:`≁`,NotTildeEqual:`≄`,NotTildeFullEqual:`≇`,NotTildeTilde:`≉`,NotVerticalBar:`∤`,Nscr:`𝒩`,Ntilde:`Ñ`,Nu:`Ν`,OElig:`Œ`,Oacute:`Ó`,Ocirc:`Ô`,Ocy:`О`,Odblac:`Ő`,Ofr:`𝔒`,Ograve:`Ò`,Omacr:`Ō`,Omega:`Ω`,Omicron:`Ο`,Oopf:`𝕆`,OpenCurlyDoubleQuote:`“`,OpenCurlyQuote:`‘`,Or:`⩔`,Oscr:`𝒪`,Oslash:`Ø`,Otilde:`Õ`,Otimes:`⨷`,Ouml:`Ö`,OverBar:`‾`,OverBrace:`⏞`,OverBracket:`⎴`,OverParenthesis:`⏜`,PartialD:`∂`,Pcy:`П`,Pfr:`𝔓`,Phi:`Φ`,Pi:`Π`,PlusMinus:`±`,Poincareplane:`ℌ`,Popf:`ℙ`,Pr:`⪻`,Precedes:`≺`,PrecedesEqual:`⪯`,PrecedesSlantEqual:`≼`,PrecedesTilde:`≾`,Prime:`″`,Product:`∏`,Proportion:`∷`,Proportional:`∝`,Pscr:`𝒫`,Psi:`Ψ`,QUOT:`"`,Qfr:`𝔔`,Qopf:`ℚ`,Qscr:`𝒬`,RBarr:`⤐`,REG:`®`,Racute:`Ŕ`,Rang:`⟫`,Rarr:`↠`,Rarrtl:`⤖`,Rcaron:`Ř`,Rcedil:`Ŗ`,Rcy:`Р`,Re:`ℜ`,ReverseElement:`∋`,ReverseEquilibrium:`⇋`,ReverseUpEquilibrium:`⥯`,Rfr:`ℜ`,Rho:`Ρ`,RightAngleBracket:`⟩`,RightArrow:`→`,RightArrowBar:`⇥`,RightArrowLeftArrow:`⇄`,RightCeiling:`⌉`,RightDoubleBracket:`⟧`,RightDownTeeVector:`⥝`,RightDownVector:`⇂`,RightDownVectorBar:`⥕`,RightFloor:`⌋`,RightTee:`⊢`,RightTeeArrow:`↦`,RightTeeVector:`⥛`,RightTriangle:`⊳`,RightTriangleBar:`⧐`,RightTriangleEqual:`⊵`,RightUpDownVector:`⥏`,RightUpTeeVector:`⥜`,RightUpVector:`↾`,RightUpVectorBar:`⥔`,RightVector:`⇀`,RightVectorBar:`⥓`,Rightarrow:`⇒`,Ropf:`ℝ`,RoundImplies:`⥰`,Rrightarrow:`⇛`,Rscr:`ℛ`,Rsh:`↱`,RuleDelayed:`⧴`,SHCHcy:`Щ`,SHcy:`Ш`,SOFTcy:`Ь`,Sacute:`Ś`,Sc:`⪼`,Scaron:`Š`,Scedil:`Ş`,Scirc:`Ŝ`,Scy:`С`,Sfr:`𝔖`,ShortDownArrow:`↓`,ShortLeftArrow:`←`,ShortRightArrow:`→`,ShortUpArrow:`↑`,Sigma:`Σ`,SmallCircle:`∘`,Sopf:`𝕊`,Sqrt:`√`,Square:`□`,SquareIntersection:`⊓`,SquareSubset:`⊏`,SquareSubsetEqual:`⊑`,SquareSuperset:`⊐`,SquareSupersetEqual:`⊒`,SquareUnion:`⊔`,Sscr:`𝒮`,Star:`⋆`,Sub:`⋐`,Subset:`⋐`,SubsetEqual:`⊆`,Succeeds:`≻`,SucceedsEqual:`⪰`,SucceedsSlantEqual:`≽`,SucceedsTilde:`≿`,SuchThat:`∋`,Sum:`∑`,Sup:`⋑`,Superset:`⊃`,SupersetEqual:`⊇`,Supset:`⋑`,THORN:`Þ`,TRADE:`™`,TSHcy:`Ћ`,TScy:`Ц`,Tab:` `,Tau:`Τ`,Tcaron:`Ť`,Tcedil:`Ţ`,Tcy:`Т`,Tfr:`𝔗`,Therefore:`∴`,Theta:`Θ`,ThickSpace:` `,ThinSpace:` `,Tilde:`∼`,TildeEqual:`≃`,TildeFullEqual:`≅`,TildeTilde:`≈`,Topf:`𝕋`,TripleDot:`⃛`,Tscr:`𝒯`,Tstrok:`Ŧ`,Uacute:`Ú`,Uarr:`↟`,Uarrocir:`⥉`,Ubrcy:`Ў`,Ubreve:`Ŭ`,Ucirc:`Û`,Ucy:`У`,Udblac:`Ű`,Ufr:`𝔘`,Ugrave:`Ù`,Umacr:`Ū`,UnderBar:`_`,UnderBrace:`⏟`,UnderBracket:`⎵`,UnderParenthesis:`⏝`,Union:`⋃`,UnionPlus:`⊎`,Uogon:`Ų`,Uopf:`𝕌`,UpArrow:`↑`,UpArrowBar:`⤒`,UpArrowDownArrow:`⇅`,UpDownArrow:`↕`,UpEquilibrium:`⥮`,UpTee:`⊥`,UpTeeArrow:`↥`,Uparrow:`⇑`,Updownarrow:`⇕`,UpperLeftArrow:`↖`,UpperRightArrow:`↗`,Upsi:`ϒ`,Upsilon:`Υ`,Uring:`Ů`,Uscr:`𝒰`,Utilde:`Ũ`,Uuml:`Ü`,VDash:`⊫`,Vbar:`⫫`,Vcy:`В`,Vdash:`⊩`,Vdashl:`⫦`,Vee:`⋁`,Verbar:`‖`,Vert:`‖`,VerticalBar:`∣`,VerticalLine:`|`,VerticalSeparator:`❘`,VerticalTilde:`≀`,VeryThinSpace:` `,Vfr:`𝔙`,Vopf:`𝕍`,Vscr:`𝒱`,Vvdash:`⊪`,Wcirc:`Ŵ`,Wedge:`⋀`,Wfr:`𝔚`,Wopf:`𝕎`,Wscr:`𝒲`,Xfr:`𝔛`,Xi:`Ξ`,Xopf:`𝕏`,Xscr:`𝒳`,YAcy:`Я`,YIcy:`Ї`,YUcy:`Ю`,Yacute:`Ý`,Ycirc:`Ŷ`,Ycy:`Ы`,Yfr:`𝔜`,Yopf:`𝕐`,Yscr:`𝒴`,Yuml:`Ÿ`,ZHcy:`Ж`,Zacute:`Ź`,Zcaron:`Ž`,Zcy:`З`,Zdot:`Ż`,ZeroWidthSpace:``,Zeta:`Ζ`,Zfr:`ℨ`,Zopf:`ℤ`,Zscr:`𝒵`,aacute:`á`,abreve:`ă`,ac:`∾`,acE:`∾̳`,acd:`∿`,acirc:`â`,acute:`´`,acy:`а`,aelig:`æ`,af:``,afr:`𝔞`,agrave:`à`,alefsym:`ℵ`,aleph:`ℵ`,alpha:`α`,amacr:`ā`,amalg:`⨿`,amp:`&`,and:`∧`,andand:`⩕`,andd:`⩜`,andslope:`⩘`,andv:`⩚`,ang:`∠`,ange:`⦤`,angle:`∠`,angmsd:`∡`,angmsdaa:`⦨`,angmsdab:`⦩`,angmsdac:`⦪`,angmsda
Line truncated
|
||||
`,Nfr:`𝔑`,NoBreak:``,NonBreakingSpace:`\xA0`,Nopf:`ℕ`,Not:`⫬`,NotCongruent:`≢`,NotCupCap:`≭`,NotDoubleVerticalBar:`∦`,NotElement:`∉`,NotEqual:`≠`,NotEqualTilde:`≂̸`,NotExists:`∄`,NotGreater:`≯`,NotGreaterEqual:`≱`,NotGreaterFullEqual:`≧̸`,NotGreaterGreater:`≫̸`,NotGreaterLess:`≹`,NotGreaterSlantEqual:`⩾̸`,NotGreaterTilde:`≵`,NotHumpDownHump:`≎̸`,NotHumpEqual:`≏̸`,NotLeftTriangle:`⋪`,NotLeftTriangleBar:`⧏̸`,NotLeftTriangleEqual:`⋬`,NotLess:`≮`,NotLessEqual:`≰`,NotLessGreater:`≸`,NotLessLess:`≪̸`,NotLessSlantEqual:`⩽̸`,NotLessTilde:`≴`,NotNestedGreaterGreater:`⪢̸`,NotNestedLessLess:`⪡̸`,NotPrecedes:`⊀`,NotPrecedesEqual:`⪯̸`,NotPrecedesSlantEqual:`⋠`,NotReverseElement:`∌`,NotRightTriangle:`⋫`,NotRightTriangleBar:`⧐̸`,NotRightTriangleEqual:`⋭`,NotSquareSubset:`⊏̸`,NotSquareSubsetEqual:`⋢`,NotSquareSuperset:`⊐̸`,NotSquareSupersetEqual:`⋣`,NotSubset:`⊂⃒`,NotSubsetEqual:`⊈`,NotSucceeds:`⊁`,NotSucceedsEqual:`⪰̸`,NotSucceedsSlantEqual:`⋡`,NotSucceedsTilde:`≿̸`,NotSuperset:`⊃⃒`,NotSupersetEqual:`⊉`,NotTilde:`≁`,NotTildeEqual:`≄`,NotTildeFullEqual:`≇`,NotTildeTilde:`≉`,NotVerticalBar:`∤`,Nscr:`𝒩`,Ntilde:`Ñ`,Nu:`Ν`,OElig:`Œ`,Oacute:`Ó`,Ocirc:`Ô`,Ocy:`О`,Odblac:`Ő`,Ofr:`𝔒`,Ograve:`Ò`,Omacr:`Ō`,Omega:`Ω`,Omicron:`Ο`,Oopf:`𝕆`,OpenCurlyDoubleQuote:`“`,OpenCurlyQuote:`‘`,Or:`⩔`,Oscr:`𝒪`,Oslash:`Ø`,Otilde:`Õ`,Otimes:`⨷`,Ouml:`Ö`,OverBar:`‾`,OverBrace:`⏞`,OverBracket:`⎴`,OverParenthesis:`⏜`,PartialD:`∂`,Pcy:`П`,Pfr:`𝔓`,Phi:`Φ`,Pi:`Π`,PlusMinus:`±`,Poincareplane:`ℌ`,Popf:`ℙ`,Pr:`⪻`,Precedes:`≺`,PrecedesEqual:`⪯`,PrecedesSlantEqual:`≼`,PrecedesTilde:`≾`,Prime:`″`,Product:`∏`,Proportion:`∷`,Proportional:`∝`,Pscr:`𝒫`,Psi:`Ψ`,QUOT:`"`,Qfr:`𝔔`,Qopf:`ℚ`,Qscr:`𝒬`,RBarr:`⤐`,REG:`®`,Racute:`Ŕ`,Rang:`⟫`,Rarr:`↠`,Rarrtl:`⤖`,Rcaron:`Ř`,Rcedil:`Ŗ`,Rcy:`Р`,Re:`ℜ`,ReverseElement:`∋`,ReverseEquilibrium:`⇋`,ReverseUpEquilibrium:`⥯`,Rfr:`ℜ`,Rho:`Ρ`,RightAngleBracket:`⟩`,RightArrow:`→`,RightArrowBar:`⇥`,RightArrowLeftArrow:`⇄`,RightCeiling:`⌉`,RightDoubleBracket:`⟧`,RightDownTeeVector:`⥝`,RightDownVector:`⇂`,RightDownVectorBar:`⥕`,RightFloor:`⌋`,RightTee:`⊢`,RightTeeArrow:`↦`,RightTeeVector:`⥛`,RightTriangle:`⊳`,RightTriangleBar:`⧐`,RightTriangleEqual:`⊵`,RightUpDownVector:`⥏`,RightUpTeeVector:`⥜`,RightUpVector:`↾`,RightUpVectorBar:`⥔`,RightVector:`⇀`,RightVectorBar:`⥓`,Rightarrow:`⇒`,Ropf:`ℝ`,RoundImplies:`⥰`,Rrightarrow:`⇛`,Rscr:`ℛ`,Rsh:`↱`,RuleDelayed:`⧴`,SHCHcy:`Щ`,SHcy:`Ш`,SOFTcy:`Ь`,Sacute:`Ś`,Sc:`⪼`,Scaron:`Š`,Scedil:`Ş`,Scirc:`Ŝ`,Scy:`С`,Sfr:`𝔖`,ShortDownArrow:`↓`,ShortLeftArrow:`←`,ShortRightArrow:`→`,ShortUpArrow:`↑`,Sigma:`Σ`,SmallCircle:`∘`,Sopf:`𝕊`,Sqrt:`√`,Square:`□`,SquareIntersection:`⊓`,SquareSubset:`⊏`,SquareSubsetEqual:`⊑`,SquareSuperset:`⊐`,SquareSupersetEqual:`⊒`,SquareUnion:`⊔`,Sscr:`𝒮`,Star:`⋆`,Sub:`⋐`,Subset:`⋐`,SubsetEqual:`⊆`,Succeeds:`≻`,SucceedsEqual:`⪰`,SucceedsSlantEqual:`≽`,SucceedsTilde:`≿`,SuchThat:`∋`,Sum:`∑`,Sup:`⋑`,Superset:`⊃`,SupersetEqual:`⊇`,Supset:`⋑`,THORN:`Þ`,TRADE:`™`,TSHcy:`Ћ`,TScy:`Ц`,Tab:` `,Tau:`Τ`,Tcaron:`Ť`,Tcedil:`Ţ`,Tcy:`Т`,Tfr:`𝔗`,Therefore:`∴`,Theta:`Θ`,ThickSpace:` `,ThinSpace:` `,Tilde:`∼`,TildeEqual:`≃`,TildeFullEqual:`≅`,TildeTilde:`≈`,Topf:`𝕋`,TripleDot:`⃛`,Tscr:`𝒯`,Tstrok:`Ŧ`,Uacute:`Ú`,Uarr:`↟`,Uarrocir:`⥉`,Ubrcy:`Ў`,Ubreve:`Ŭ`,Ucirc:`Û`,Ucy:`У`,Udblac:`Ű`,Ufr:`𝔘`,Ugrave:`Ù`,Umacr:`Ū`,UnderBar:`_`,UnderBrace:`⏟`,UnderBracket:`⎵`,UnderParenthesis:`⏝`,Union:`⋃`,UnionPlus:`⊎`,Uogon:`Ų`,Uopf:`𝕌`,UpArrow:`↑`,UpArrowBar:`⤒`,UpArrowDownArrow:`⇅`,UpDownArrow:`↕`,UpEquilibrium:`⥮`,UpTee:`⊥`,UpTeeArrow:`↥`,Uparrow:`⇑`,Updownarrow:`⇕`,UpperLeftArrow:`↖`,UpperRightArrow:`↗`,Upsi:`ϒ`,Upsilon:`Υ`,Uring:`Ů`,Uscr:`𝒰`,Utilde:`Ũ`,Uuml:`Ü`,VDash:`⊫`,Vbar:`⫫`,Vcy:`В`,Vdash:`⊩`,Vdashl:`⫦`,Vee:`⋁`,Verbar:`‖`,Vert:`‖`,VerticalBar:`∣`,VerticalLine:`|`,VerticalSeparator:`❘`,VerticalTilde:`≀`,VeryThinSpace:` `,Vfr:`𝔙`,Vopf:`𝕍`,Vscr:`𝒱`,Vvdash:`⊪`,Wcirc:`Ŵ`,Wedge:`⋀`,Wfr:`𝔚`,Wopf:`𝕎`,Wscr:`𝒲`,Xfr:`𝔛`,Xi:`Ξ`,Xopf:`𝕏`,Xscr:`𝒳`,YAcy:`Я`,YIcy:`Ї`,YUcy:`Ю`,Yacute:`Ý`,Ycirc:`Ŷ`,Ycy:`Ы`,Yfr:`𝔜`,Yopf:`𝕐`,Yscr:`𝒴`,Yuml:`Ÿ`,ZHcy:`Ж`,Zacute:`Ź`,Zcaron:`Ž`,Zcy:`З`,Zdot:`Ż`,ZeroWidthSpace:``,Zeta:`Ζ`,Zfr:`ℨ`,Zopf:`ℤ`,Zscr:`𝒵`,aacute:`á`,abreve:`ă`,ac:`∾`,acE:`∾̳`,acd:`∿`,acirc:`â`,acute:`´`,acy:`а`,aelig:`æ`,af:``,afr:`𝔞`,agrave:`à`,alefsym:`ℵ`,aleph:`ℵ`,alpha:`α`,amacr:`ā`,amalg:`⨿`,amp:`&`,and:`∧`,andand:`⩕`,andd:`⩜`,andslope:`⩘`,andv:`⩚`,ang:`∠`,ange:`⦤`,angle:`∠`,angmsd:`∡`,angmsdaa:`⦨`,angmsdab:`⦩`,angmsdac:`⦪`,angmsda
Line truncated
|
||||
`)}},DW.defaultYaml={explicit:!1,version:`1.2`},DW.defaultTags={"!!":`tag:yaml.org,2002:`}}));function gbt(e){if(/[\x00-\x19\s,[\]{}]/.test(e)){let t=`Anchor must not contain whitespace or control characters: ${JSON.stringify(e)}`;throw Error(t)}return!0}function _bt(e){let t=new Set;return yW(e,{Value(e,n){n.anchor&&t.add(n.anchor)}}),t}function vbt(e,t){for(let n=1;;++n){let r=`${e}${n}`;if(!t.has(r))return r}}function ybt(e,t){let n=[],r=new Map,i=null;return{onAnchor:r=>{n.push(r),i??=_bt(e);let a=vbt(t,i);return i.add(a),a},setAnchors:()=>{for(let e of n){let t=r.get(e);if(typeof t==`object`&&t.anchor&&(hW(t.node)||nW(t.node)))t.node.anchor=t.anchor;else{let t=Error(`Failed to resolve repeated object (this should not happen)`);throw t.source=e,t}}},sourceObjects:r}}var OW=s((()=>{vW(),EW()}));function kW(e,t,n,r){if(r&&typeof r==`object`)if(Array.isArray(r))for(let t=0,n=r.length;t<n;++t){let n=r[t],i=kW(e,r,String(t),n);i===void 0?delete r[t]:i!==n&&(r[t]=i)}else if(r instanceof Map)for(let t of Array.from(r.keys())){let n=r.get(t),i=kW(e,r,t,n);i===void 0?r.delete(t):i!==n&&r.set(t,i)}else if(r instanceof Set)for(let t of Array.from(r)){let n=kW(e,r,t,t);n===void 0?r.delete(t):n!==t&&(r.delete(t),r.add(n))}else for(let[t,n]of Object.entries(r)){let i=kW(e,r,t,n);i===void 0?delete r[t]:i!==n&&(r[t]=i)}return e.call(t,n,r)}var bbt=s((()=>{}));function AW(e,t,n){if(Array.isArray(e))return e.map((e,t)=>AW(e,String(t),n));if(e&&typeof e.toJSON==`function`){if(!n||!_W(e))return e.toJSON(t,n);let r={aliasCount:0,count:1,res:void 0};n.anchors.set(e,r),n.onCreate=e=>{r.res=e,delete n.onCreate};let i=e.toJSON(t,n);return n.onCreate&&n.onCreate(i),i}return typeof e==`bigint`&&!n?.keep?Number(e):e}var jW=s((()=>{vW()})),MW,NW=s((()=>{bbt(),vW(),jW(),MW=class{constructor(e){Object.defineProperty(this,uW,{value:e})}clone(){let e=Object.create(Object.getPrototypeOf(this),Object.getOwnPropertyDescriptors(this));return this.range&&(e.range=this.range.slice()),e}toJS(e,{mapAsMap:t,maxAliasCount:n,onAnchor:r,reviver:i}={}){if(!fW(e))throw TypeError(`A document argument is required`);let a={anchors:new Map,doc:e,keep:!0,mapAsMap:t===!0,mapKeyWarned:!1,maxAliasCount:typeof n==`number`?n:100},o=AW(this,``,a);if(typeof r==`function`)for(let{count:e,res:t}of a.anchors.values())r(t,e);return typeof i==`function`?kW(i,{"":o},``,o):o}}}));function PW(e,t,n){if(dW(t)){let r=t.resolve(e),i=n&&r&&n.get(r);return i?i.count*i.aliasCount:0}else if(nW(t)){let r=0;for(let i of t.items){let t=PW(e,i,n);t>r&&(r=t)}return r}else if(mW(t)){let r=PW(e,t.key,n),i=PW(e,t.value,n);return Math.max(r,i)}return 1}var FW,IW=s((()=>{OW(),EW(),vW(),NW(),jW(),FW=class extends MW{constructor(e){super(iW),this.source=e,Object.defineProperty(this,"tag",{set(){throw Error(`Alias nodes cannot have tags`)}})}resolve(e,t){if(t?.maxAliasCount===0)throw ReferenceError(`Alias resolution is disabled`);let n;t?.aliasResolveCache?n=t.aliasResolveCache:(n=[],yW(e,{Node:(e,t)=>{(dW(t)||_W(t))&&n.push(t)}}),t&&(t.aliasResolveCache=n));let r;for(let e of n){if(e===this)break;e.anchor===this.source&&(r=e)}return r}toJSON(e,t){if(!t)return{source:this.source};let{anchors:n,doc:r,maxAliasCount:i}=t,a=this.resolve(r,t);if(!a){let e=`Unresolved alias (the anchor must be set before the alias): ${this.source}`;throw ReferenceError(e)}let o=n.get(a);if(o||=(AW(a,null,t),n.get(a)),o?.res===void 0)throw ReferenceError(`This should not happen: Alias anchor was not resolved?`);if(i>=0&&(o.count+=1,o.aliasCount===0&&(o.aliasCount=PW(r,a,n)),o.count*o.aliasCount>i))throw ReferenceError(`Excessive alias count indicates a resource exhaustion attack`);return o.res}toString(e,t,n){let r=`*${this.source}`;if(e){if(gbt(this.source),e.options.verifyAliasOrder&&!e.anchors.has(this.source)){let e=`Unresolved alias (the anchor must be set before the alias): ${this.source}`;throw Error(e)}if(e.implicitKey)return`${r} `}return r}}})),LW,RW,zW=s((()=>{vW(),NW(),jW(),LW=e=>!e||typeof e!=`function`&&typeof e!=`object`,RW=class extends MW{constructor(e){super(cW),this.value=e}toJSON(e,t){return t?.keep?this.value:AW(this.value,e,t)}toString(){return String(this.value)}},RW.BLOCK_FOLDED=`BLOCK_FOLDED`,RW.BLOCK_LITERAL=`BLOCK_LITERAL`,RW.PLAIN=`PLAIN`,RW.QUOTE_DOUBLE=`QUOTE_DOUBLE`,RW.QUOTE_SINGLE=`QUOTE_SINGLE`}));function xbt(e,t,n){if(t){let e=n.filter(e=>e.tag===t),r=e.find(e=>!e.format)??e[0];if(!r)throw Error(`Tag ${t} not found`);return r}return n.find(t=>t.identify?.(e)&&!t.format)}function BW(e,t,n){if(fW(e)&&(e=e.contents),rW(e))return e;if(mW(e)){let t=n.schema[oW].createNode?.(n.schema,null,n);return t.items.push(e),t}(e instanceof String||e instanceof Number||e instanceof Boolean||typeof BigInt<`u`&&e instanceof BigInt)&&(e=e.valueOf());let{aliasDuplicateObjects:r,onAnchor:i,onTagObj:a,schema:o,sourceObjects:s}=n,c;if(r&&e&&typeof e==`object`){if(c=s.get(e),c)return c.anchor??=i(e),new FW(c.anchor);c={anchor:null,node:null},s.set(e,c)}t?.startsWith(`!!`)&&(t=Sbt+t.slice(2));let l=xbt
Line truncated
|
||||
`)?KW(n,t):n.includes(`
|
||||
`)?`
|
||||
@@ -2360,11 +2360,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
||||
version: "1.0.0"`})})})),Q5t,$5t=s((()=>{_e(),K2(),Q5t=T({"x-scalar-watch-mode":E(C({typeComment:`When true, the document is watched for external file changes`}))},{typeName:`XScalarWatchMode`,typeComment:G2(`Whether the document is in watch mode (reloads when the source file changes).`,{language:`yaml`,body:`x-scalar-watch-mode: true`})})})),e7t,t7t=s((()=>{_e(),K2(),e7t=T({"x-tags":E(de(S(),{typeComment:`Ordered list of tag names for this schema object`}))},{typeName:`XTags`,typeComment:G2(`Custom tag ordering hints for schema objects in the sidebar.`,{language:`yaml`,body:`x-tags:
|
||||
- users
|
||||
- admin`})})})),n7t=s((()=>{I5t(),R5t(),_e(),K2(),B5t(),H5t(),W5t(),K5t(),J5t(),Z5t(),$5t(),t7t()})),r7t,i7t=s((()=>{_e(),K2(),r7t=T({"x-scalar-selected-server":E(S({typeComment:"The currently selected server. For OpenAPI documents this is the server URL; for AsyncAPI documents this is the server name (key in `document.servers`)."}))},{typeName:`XScalarSelectedServer`,typeComment:G2("The currently selected server for this API description. For OpenAPI documents the value is the server URL; for AsyncAPI documents the value is the server name (key in `document.servers`).",{language:`yaml`,body:`x-scalar-selected-server: https://api.example.com`})})})),a7t=s((()=>{i7t()})),o7t,s7t=s((()=>{_e(),K2(),o7t=T({"x-additionalPropertiesName":E(S({typeComment:`Human-readable label for additional properties on this schema`}))},{typeName:`XAdditionalPropertiesName`,typeComment:G2(`Display name for additional properties on a schema object.`,{language:`yaml`,body:`x-additionalPropertiesName: metadata`})})})),q2,c7t,l7t=s((()=>{_e(),K2(),q2=fe([w(S(),S()),de(S())]),c7t=T({"x-enumDescriptions":E(q2,{typeComment:`Map or list of descriptions keyed by enum value (camelCase spelling)`}),"x-enum-descriptions":E(q2,{typeComment:`Map or list of descriptions keyed by enum value (kebab-case spelling)`})},{typeName:`XEnumDescriptions`,typeComment:G2(`Descriptions for enum values. Keys must match enum values.`,{language:`yaml`,body:`x-enumDescriptions:
|
||||
other: Other reason`})})})),u7t,d7t=s((()=>{_e(),K2(),u7t=T({"x-enum-varnames":E(de(S(),{typeComment:"Display names for enum values (same order as `enum`)"})),"x-enumNames":E(de(S(),{typeComment:`Alias for x-enum-varnames — display names for enum values`}))},{typeName:`XEnumVarNames`,typeComment:G2(`Display names for enum values. Must match the order of the ${N5t(`enum`)} array.`,{language:`yaml`,body:`enum: [moon, asteroid]
|
||||
other: Other reason`})})})),u7t,d7t=s((()=>{_e(),K2(),u7t=T({"x-enum-varnames":E(de(S(),{typeComment:"Display names for enum values (same order as `enum`)"})),"x-enumNames":E(de(S(),{typeComment:`Alias for x-enum-varnames - display names for enum values`}))},{typeName:`XEnumVarNames`,typeComment:G2(`Display names for enum values. Must match the order of the ${N5t(`enum`)} array.`,{language:`yaml`,body:`enum: [moon, asteroid]
|
||||
x-enum-varnames: [Moon, Asteroid]`})})})),f7t,p7t=s((()=>{_e(),K2(),f7t=T({"x-examples":E(w(S(),ce(),{typeComment:`Map of example name to example value`}))},{typeName:`XExamples`,typeComment:G2(`Named examples attached to a schema. Keys are example names; values are the example payloads.`,{language:`yaml`,body:`x-examples:
|
||||
user:
|
||||
id: 1
|
||||
name: Ada`})})})),m7t,h7t=s((()=>{_e(),K2(),m7t=T({"x-variable":E(S({typeComment:`Variable name used for substitution in the API client`}))},{typeName:`XVariable`,typeComment:G2(`References a variable for schema property substitution in the API client.`,{language:`yaml`,body:`x-variable: userId`})})})),g7t=s((()=>{s7t(),l7t(),d7t(),p7t(),h7t()})),_7t,v7t=s((()=>{_e(),_7t=T({propertyName:S({typeComment:`REQUIRED. The name of the property in the payload that will hold the discriminating value. This property SHOULD be required in the payload schema, as the behavior when the property is absent is undefined.`}),mapping:E(w(S(),S(),{typeComment:`An object to hold mappings between payload values and schema names or URI references.`}))},{typeName:`DiscriminatorObject`})})),y7t,b7t=s((()=>{_e(),y7t=T({url:S({typeComment:`REQUIRED. The URI for the target documentation. This MUST be in the form of a URI.`}),description:E(S({typeComment:`A description of the target documentation. CommonMark syntax MAY be used for rich text representation.`}))},{typeName:`ExternalDocumentationObject`})})),x7t,S7t=s((()=>{_e(),x7t=T({$status:E(fe([D(`loading`),D(`error`)]),{typeComment:`Indicates the current status of the reference resolution. Can be either 'loading' while fetching the reference or 'error' if the resolution failed.`}),$global:E(C({typeComment:`Indicates whether this reference should be resolved globally across all documents, rather than just within the current document context.`}))},{typeName:`ReferenceObjectExtensions`})})),C7t,J2,w7t=s((()=>{_e(),C7t=T({$ref:S({typeComment:`REQUIRED. The reference identifier. This MUST be in the form of a URI.`}),summary:E(S({typeComment:`A short summary which by default SHOULD override that of the referenced component. If the referenced object-type does not allow a summary field, then this field has no effect.`})),description:E(S({typeComment:`A description which by default SHOULD override that of the referenced component. CommonMark syntax MAY be used for rich text representation. If the referenced object-type does not allow a description field, then this field has no effect.`}))},{typeName:`ReferenceObject`}),J2=e=>fe([C7t,e])})),T7t,E7t=s((()=>{_e(),T7t=T({name:E(S({typeComment:`Replaces the name of the element/attribute used for the described schema property. When defined within items, it will affect the name of the individual XML elements within the list. When defined alongside type being "array" (outside the items), it will affect the wrapping element if and only if wrapped is true. If wrapped is false, it will be ignored.`})),namespace:E(S({typeComment:`The URI of the namespace definition. Value MUST be in the form of a non-relative URI.`})),prefix:E(S({typeComment:`The prefix to be used for the name.`})),attribute:E(C({typeComment:`Declares whether the property definition translates to an attribute instead of an element. Default value is false.`})),wrapped:E(C({typeComment:`MAY be used only for an array definition. Signifies whether the array is wrapped (for example, <books><book/><book/></books>) or unwrapped (<book/><book/>). Default value is false. The definition takes effect only when defined alongside type being "array" (outside the items).`}))},{typeName:`XMLObject`})})),D7t,O7t,Y2,k7t,X2,A7t,Z2,j7t,Q2,M7t,N7t,P7t,F7t,$2,I7t=s((()=>{_e(),n7t(),g7t(),v7t(),b7t(),w7t(),E7t(),D7t=[z5t,F5t,m7t,f7t,c7t,u7t,o7t,e7t],O7t=T({name:E(S({typeComment:`Schema name (extension).`})),title:E(S({typeComment:`A title for the schema.`})),description:E(S({typeComment:`A description of the schema.`})),default:E(ce({typeComment:`Default value for the schema.`})),enum:E(de(ce(),{typeComment:`Array of allowed values.`,typeName:`JsonSchemaEnum`})),const:E(ce({typeComment:`Constant value that must match exactly.`})),contentMediaType:E(S({typeComment:`Media type for content validation.`})),contentEncoding:E(S({typeComment:`Content encoding.`})),contentSchema:E(J2(me(()=>$2))),deprecated:E(C({typeComment:`Whether the schema is deprecated.`})),discriminator:E(_7t),readOnly:E(C({typeComment:`Whether the schema is read-only.`})),writeOnly:E(C({typeComment:`Whether the schema is write-only.`})),xml:E(T7t),externalDocs:E(y7t),example:E(ce({typeComment:`A free-form field to include an example of an instance for this schema. Deprecated in favor of the JSON Schema examples keyword.`})),examples:E(de(ce(),{typeComment:`An array of examples of valid instances for this schema. This keyword follows the JSON Schema Draft 2020-12 specification.`,typeName:`SchemaExamplesArray`})),allOf:E(de(J2(me(()=>$2)),{typeName:`SchemaObjectAllOf`})),oneOf:E(de(J2(me(()=>$2)),{typeName:`SchemaObjectOneOf`})),anyOf:E(de(J2(me(()=>$2)),{typeName:`SchemaObjectAnyOf`})),not:E(J2(me(()=>$2)))}),Y2=T({multipleOf:E(oe({typeComment:`Number must be a multiple of this value.`})),maximum:E(oe({typeComment:`Maximum value (inclusive).`})),exclusiveMaximum:E(oe({typeComment:`Maximum value (exclusive).`})),minimum:E(oe({typeComment:`MiLine truncated
|
||||
name: Ada`})})})),m7t,h7t=s((()=>{_e(),K2(),m7t=T({"x-variable":E(S({typeComment:`Variable name used for substitution in the API client`}))},{typeName:`XVariable`,typeComment:G2(`References a variable for schema property substitution in the API client.`,{language:`yaml`,body:`x-variable: userId`})})})),g7t=s((()=>{s7t(),l7t(),d7t(),p7t(),h7t()})),_7t,v7t=s((()=>{_e(),_7t=T({propertyName:S({typeComment:`REQUIRED. The name of the property in the payload that will hold the discriminating value. This property SHOULD be required in the payload schema, as the behavior when the property is absent is undefined.`}),mapping:E(w(S(),S(),{typeComment:`An object to hold mappings between payload values and schema names or URI references.`}))},{typeName:`DiscriminatorObject`})})),y7t,b7t=s((()=>{_e(),y7t=T({url:S({typeComment:`REQUIRED. The URI for the target documentation. This MUST be in the form of a URI.`}),description:E(S({typeComment:`A description of the target documentation. CommonMark syntax MAY be used for rich text representation.`}))},{typeName:`ExternalDocumentationObject`})})),x7t,S7t=s((()=>{_e(),x7t=T({$status:E(fe([D(`loading`),D(`error`)]),{typeComment:`Indicates the current status of the reference resolution. Can be either 'loading' while fetching the reference or 'error' if the resolution failed.`}),$global:E(C({typeComment:`Indicates whether this reference should be resolved globally across all documents, rather than just within the current document context.`}))},{typeName:`ReferenceObjectExtensions`})})),C7t,J2,w7t=s((()=>{_e(),C7t=T({$ref:S({typeComment:`REQUIRED. The reference identifier. This MUST be in the form of a URI.`}),summary:E(S({typeComment:`A short summary which by default SHOULD override that of the referenced component. If the referenced object-type does not allow a summary field, then this field has no effect.`})),description:E(S({typeComment:`A description which by default SHOULD override that of the referenced component. CommonMark syntax MAY be used for rich text representation. If the referenced object-type does not allow a description field, then this field has no effect.`}))},{typeName:`ReferenceObject`}),J2=e=>fe([C7t,e])})),T7t,E7t=s((()=>{_e(),T7t=T({name:E(S({typeComment:`Replaces the name of the element/attribute used for the described schema property. When defined within items, it will affect the name of the individual XML elements within the list. When defined alongside type being "array" (outside the items), it will affect the wrapping element if and only if wrapped is true. If wrapped is false, it will be ignored.`})),namespace:E(S({typeComment:`The URI of the namespace definition. Value MUST be in the form of a non-relative URI.`})),prefix:E(S({typeComment:`The prefix to be used for the name.`})),attribute:E(C({typeComment:`Declares whether the property definition translates to an attribute instead of an element. Default value is false.`})),wrapped:E(C({typeComment:`MAY be used only for an array definition. Signifies whether the array is wrapped (for example, <books><book/><book/></books>) or unwrapped (<book/><book/>). Default value is false. The definition takes effect only when defined alongside type being "array" (outside the items).`}))},{typeName:`XMLObject`})})),D7t,O7t,Y2,k7t,X2,A7t,Z2,j7t,Q2,M7t,N7t,P7t,F7t,$2,I7t=s((()=>{_e(),n7t(),g7t(),v7t(),b7t(),w7t(),E7t(),D7t=[z5t,F5t,m7t,f7t,c7t,u7t,o7t,e7t],O7t=T({name:E(S({typeComment:`Schema name (extension).`})),title:E(S({typeComment:`A title for the schema.`})),description:E(S({typeComment:`A description of the schema.`})),default:E(ce({typeComment:`Default value for the schema.`})),enum:E(de(ce(),{typeComment:`Array of allowed values.`,typeName:`JsonSchemaEnum`})),const:E(ce({typeComment:`Constant value that must match exactly.`})),contentMediaType:E(S({typeComment:`Media type for content validation.`})),contentEncoding:E(S({typeComment:`Content encoding.`})),contentSchema:E(J2(me(()=>$2))),deprecated:E(C({typeComment:`Whether the schema is deprecated.`})),discriminator:E(_7t),readOnly:E(C({typeComment:`Whether the schema is read-only.`})),writeOnly:E(C({typeComment:`Whether the schema is write-only.`})),xml:E(T7t),externalDocs:E(y7t),example:E(ce({typeComment:`A free-form field to include an example of an instance for this schema. Deprecated in favor of the JSON Schema examples keyword.`})),examples:E(de(ce(),{typeComment:`An array of examples of valid instances for this schema. This keyword follows the JSON Schema Draft 2020-12 specification.`,typeName:`SchemaExamplesArray`})),allOf:E(de(J2(me(()=>$2)),{typeName:`SchemaObjectAllOf`})),oneOf:E(de(J2(me(()=>$2)),{typeName:`SchemaObjectOneOf`})),anyOf:E(de(J2(me(()=>$2)),{typeName:`SchemaObjectAnyOf`})),not:E(J2(me(()=>$2)))}),Y2=T({multipleOf:E(oe({typeComment:`Number must be a multiple of this value.`})),maximum:E(oe({typeComment:`Maximum value (inclusive).`})),exclusiveMaximum:E(oe({typeComment:`Maximum value (exclusive).`})),minimum:E(oe({typeComment:`MiLine truncated
|
||||
`||e===`\r`,Een=e=>e===`{`||e===`[`,Den=(e,t,n)=>{let r;for(let i=n-1;i>t;--i){let t=e[i];if(t===`\r`)return;if(t===`
|
||||
`)return Oen(r);if(r===void 0)r=t;else if(r[0]===t)r+=t;else return}},Oen=e=>e===void 0?0:e[0]===` `?e.length:e})),Aen,f3,jen,Men,Nen=s((()=>{Aen=(e,t)=>{let n=f3(e,`toPrecision`,t,t);return n===void 0?f3(e,`toExponential`,t,t):n},f3=(e,t,n,r)=>{let i=e[t](r).replace(jen,`$1`).replace(Men,`$1`);return i.length<=n?i:r===1?void 0:f3(e,t,n,r-1)},jen=/(e)\+/iu,Men=/\.?0*($|e)/iu})),Pen,Fen,Ien,Len=s((()=>{Pen=(e,t)=>{if(typeof e!=`string`)throw TypeError(`Input must be a JSON string: ${e}`);Fen(t)},Fen=e=>{if(Ien(e),e<0)throw TypeError(`"maxSize" argument must be positive: ${e}`);if(e<7)throw TypeError(`"maxSize" argument must be at least 7: ${e}`)},Ien=e=>{if(e===void 0)throw TypeError(`"maxSize" argument must be defined`);if(!Number.isInteger(e))throw TypeError(`"maxSize" argument must be an integer: ${e}`)}})),p3,m3,h3,Ren,zen,Ben,Ven,g3,Hen,Uen,Wen,Gen,Ken,qen,Jen=s((()=>{p3=(e,t,n)=>{let r=g3(t,n);return m3(e,t,r)},m3=(e,t,n)=>{if(n>=t)return n;let r=e[n];return r>=Ken&&r<=qen?m3(e,t,n+1):n},h3=(e,t,n)=>{if(n===void 0)return n;let r=g3(t,n);return Ren(e,r)},Ren=(e,t)=>zen(e,t)?t-3:Ben(e,t)?t-2:Ven(e,t)?t-1:t,zen=(e,t)=>t>=3&&e[t-3]>=Hen&&e[t-3]<=Uen,Ben=(e,t)=>t>=2&&e[t-2]>=Wen,Ven=(e,t)=>t>=1&&e[t-1]>=Gen,g3=(e,t)=>t<0||Object.is(t,-0)?Math.max(e+t,0):t,Hen=240,Uen=244,Wen=224,Gen=194,Ken=128,qen=191})),Yen,Xen=s((()=>{Jen(),Yen=(e,t,n)=>{let r=globalThis.Buffer.from(e),i=p3(r,r.length,t),a=h3(r,r.length,n);return i===0&&a>=r.length?r.toString():r.toString(`utf8`,i,a)}})),_3,v3,y3,b3,Zen,x3=s((()=>{_3=55296,v3=56319,y3=56320,b3=57343,Zen=/[\uD800-\uDFFF]/gu})),S3,Qen,$en=s((()=>{x3(),S3=e=>Qen(e)?e.replace(Zen,`�`):e,Qen=e=>{for(let t=0;t<e.length;t+=1){let n=e.codePointAt(t);if(n>=55296&&n<=57343)return!0}return!1}})),C3,etn=s((()=>{x3(),C3=({input:e,targetByteCount:t,firstStartSurrogate:n,lastStartSurrogate:r,firstEndSurrogate:i,lastEndSurrogate:a,increment:o,canBacktrack:s,shift:c,charIndexInit:l})=>{let u=l,d=u,f=0;for(;f<t;u+=o){d=u;let t=e.charCodeAt(u);if(Number.isNaN(t))break;if(t<=127){f+=1;continue}if(t<=2047){f+=2;continue}if(f+=3,t<n||t>r)continue;let s=e.charCodeAt(u+o);Number.isNaN(s)||s<i||s>a||(f+=1,u+=o)}return(s&&f>t?d:u)+c}})),w3,ttn,ntn,rtn=s((()=>{x3(),etn(),w3=(e,t,n)=>t<0||Object.is(t,-0)?ntn(e,t,n):ttn(e,t,n),ttn=(e,t,n)=>C3({input:e,targetByteCount:t,firstStartSurrogate:_3,lastStartSurrogate:v3,firstEndSurrogate:y3,lastEndSurrogate:b3,increment:1,canBacktrack:n,shift:0,charIndexInit:0}),ntn=(e,t,n)=>C3({input:e,targetByteCount:-t,firstStartSurrogate:y3,lastStartSurrogate:b3,firstEndSurrogate:_3,lastEndSurrogate:v3,increment:-1,canBacktrack:!n,shift:1,charIndexInit:e.length-1})})),T3,itn,atn=s((()=>{$en(),rtn(),T3=(e,t,n)=>{let r=w3(e,t,!1),i=itn(e,n);return S3(r===0&&i===void 0?e:e.slice(r,i))},itn=(e,t)=>{if(t===void 0)return t;let n=w3(e,t,!0);return n===e.length?void 0:n}})),otn,stn,E3,ctn,ltn,D3,utn=s((()=>{Jen(),otn=(e,t,n)=>{let{textEncoder:r,textDecoder:i}=stn(),a=ltn(e),{written:o}=r.encodeInto(e,a),s=p3(a,o,t),c=h3(a,o,n),l=c===void 0?o:Math.min(c,o),u=a.subarray(s,l);return i.decode(u)},stn=()=>(E3===void 0&&(E3=new globalThis.TextEncoder,ctn=new globalThis.TextDecoder(`utf8`,{fatal:!1})),{textEncoder:E3,textDecoder:ctn}),ltn=e=>{let t=e.length*3;return t>1e5?new Uint8Array(t):((D3===void 0||D3.length<t)&&(D3=new Uint8Array(t)),D3)}})),dtn,O3,k3,ftn=s((()=>{dtn=(e,t)=>{if(t===void 0)return t;let n=O3(e,t);return n>=e.length*k3?void 0:n},O3=(e,t)=>t<=e.length*-k3?0:t,k3=4})),ptn,mtn,htn,A3,gtn=s((()=>{ptn=(e,t,n)=>{if(typeof e!=`string`)throw TypeError(`First argument must be a string: ${e}`);mtn(t),htn(n)},mtn=e=>{if(e===void 0)throw TypeError(`Second argument is required.`);A3(`Second`,e)},htn=e=>{e!==void 0&&A3(`Third`,e)},A3=(e,t)=>{if(!Number.isInteger(t))throw TypeError(`${e} argument must be an integer: ${t}`)}})),_tn,vtn,j3,ytn=s((()=>{x3(),_tn=e=>{let t=!0,n=0;for(let r=0;r<j3;r+=1){let i=vtn(e,r);i<=127||(t&&=!1,i>2047&&(n+=1))}return{asciiOnly:t,longCharsPercentage:n/j3}},vtn=(e,t)=>{let n=j3-1,r=1-(n-t)/n,i=Math.round(r*(e.length-1));return e.charCodeAt(i)},j3=50})),btn,xtn,Stn,Ctn,wtn,M3,Ttn=s((()=>{Xen(),atn(),utn(),ftn(),$en(),gtn(),ytn(),btn=(e,t,n)=>{if(ptn(e,t,n),e===``)return e;let r=O3(e,t),i=dtn(e,n);return i===void 0&&Object.is(r,0)?S3(e):xtn(e,r,i)},xtn=(e,t,n)=>{if(e.length<=Stn)return T3(e,t,n);let{asciiOnly:r,longCharsPercentage:i}=_tn(e);return r?wtn(e,t,n):i>=Ctn?T3(e,t,n):M3(e,t,n)},Stn=200,Ctn=.4,wtn=(e,t,n)=>`Buffer`in globalThis&&`from`in globalThis.Buffer?Yen(e,t,n):M3(e,t,n),M3=(e,t,n)=>`TextEncoder`in globalThis?otn(e,t,n):T3(e,t,n)})),Etn,Dtn,Otn,ktn,Atn,N3,P3,jtn=s((()=>{Ttn(),Etn=(e,t)=>{let n=JSON.stringify(e),r=btn(ktn(n),0,t-P3.length-N3.length*2),i=`${Dtn(r)}${P3}`;return Atn(i)},Dtn=e=>e.replace(Otn,``),Otn=/(\\|\\u[0-9a-fA-F]{0,3})$/u,ktn=e=>e.slice(N3.length,-N3.length),Atn=e=>`${N3}${e}${N3}`,N3=`"`,P3=`...`})),Mtn,Ntn=s((()=>{Mtn=e=>globalThis.Buffer.byteLength(e)})),F3,Ptn,Ftn,Itn,Ltn,Rtn,ztn,Btn=s((()=>{F3=e=>{let t=e.le
Line truncated
|
||||
Error message: ${Vnn(t)}`,cause:t}),this[ern]=!0,this.text=e}static isInstance(e){return a6.hasMarker(e,l6)}},nrn=`AI_LoadAPIKeyError`,`${nrn}`,rrn=`AI_LoadSettingError`,`${rrn}`,irn=`AI_NoContentGeneratedError`,`${irn}`,arn=`AI_NoSuchModelError`,`${arn}`,orn=`AI_TooManyEmbeddingValuesForCallError`,`${orn}`,d6=`AI_TypeValidationError`,f6=`vercel.ai.error.${d6}`,srn=Symbol.for(f6),p6=class e extends (lrn=a6,crn=srn,lrn){constructor({value:e,cause:t}){super({name:d6,message:`Type validation failed: Value: ${JSON.stringify(e)}.
|
||||
|
||||
@@ -70,7 +70,7 @@ func RegisterRoutes(r *gin.Engine) {
|
||||
|
||||
apiGroup.GET("/servers", listServers)
|
||||
// Static segment, registered alongside /servers/:id exactly as
|
||||
// /servers/new already is — gin resolves statics ahead of wildcards.
|
||||
// /servers/new already is - gin resolves statics ahead of wildcards.
|
||||
apiGroup.GET("/servers/tags", listKnownTags)
|
||||
apiGroup.POST("/servers", createServer)
|
||||
apiGroup.GET("/servers/new", newServer)
|
||||
@@ -132,7 +132,7 @@ func RegisterRoutes(r *gin.Engine) {
|
||||
// handler answers every GET with a hardcoded 405, because a stateless
|
||||
// server has no session to open the server-to-client SSE stream against.
|
||||
// That 405 is the protocol-correct response for an MCP server that offers
|
||||
// no SSE leg — an unregistered GET would 404 instead, which a client reads
|
||||
// no SSE leg - an unregistered GET would 404 instead, which a client reads
|
||||
// as "no MCP endpoint here at all" rather than "this one is POST-only".
|
||||
// This route is not a working GET; it exists solely to produce that 405.
|
||||
mcpGroup := apiGroup.Group("/mcp", RequireFeature(license.FeatureMCP))
|
||||
@@ -576,11 +576,11 @@ func getKey(c *gin.Context) {
|
||||
all, _ := services.GetAssignmentsWithServers(auth.InstanceID(c), id)
|
||||
|
||||
// A tag-restricted token may legitimately hold a key that is also
|
||||
// assigned to a server outside its restriction — the key itself is
|
||||
// assigned to a server outside its restriction - the key itself is
|
||||
// still returned above. Only the assignment list is filtered, and
|
||||
// silently: an assignment whose Server is nil or out of scope is
|
||||
// dropped rather than kept with the hostname redacted, so the response
|
||||
// gives no signal — not even a count — of what was removed.
|
||||
// gives no signal - not even a count - of what was removed.
|
||||
scope := auth.ServerScope(c)
|
||||
assignments := make([]services.AssignmentWithServer, 0, len(all))
|
||||
for _, a := range all {
|
||||
@@ -778,7 +778,7 @@ func applyUpdates(c *gin.Context) {
|
||||
// downloads and installs the latest agent. Deliberately not in the generated
|
||||
// OpenAPI document: it is registered on the bare engine, not under the /api
|
||||
// group the document's BasePath assumes, so a @Router annotation here would
|
||||
// publish /api/update — a path that 404s — rather than the real top-level
|
||||
// publish /api/update - a path that 404s - rather than the real top-level
|
||||
// /update. It serves a shell script, not JSON, so there is nothing lost by
|
||||
// leaving it out of a JSON API reference.
|
||||
func handleUpdateScript(c *gin.Context) {
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
//
|
||||
// /healthz is liveness: the process is up and serving. It touches nothing
|
||||
// external, because a Mongo outage must not make Kubernetes restart every
|
||||
// server pod — a restart loop cannot fix someone else's database, and it
|
||||
// server pod - a restart loop cannot fix someone else's database, and it
|
||||
// destroys every open command stream and console session on the way.
|
||||
//
|
||||
// /readyz is readiness: this pod can serve a request end to end, which needs
|
||||
|
||||
@@ -48,7 +48,7 @@ func licenceExempt(c *gin.Context) bool {
|
||||
// RequireActiveLicense blocks mutating requests when the licence is not valid.
|
||||
//
|
||||
// Mounted on the /api group, so a route added tomorrow is gated because of where
|
||||
// it lives rather than because someone remembered. GET and HEAD always pass —
|
||||
// it lives rather than because someone remembered. GET and HEAD always pass -
|
||||
// reading is never blocked.
|
||||
func RequireActiveLicense() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
const publicStatusRateLimit = 120
|
||||
|
||||
// RateLimitPublicStatus counts requests per client address in a one-minute
|
||||
// fixed window, exactly as RateLimitTokens does — including the part that
|
||||
// fixed window, exactly as RateLimitTokens does - including the part that
|
||||
// matters most: when Redis is unavailable it allows rather than denies. A
|
||||
// status page must survive the outage it exists to report.
|
||||
func RateLimitPublicStatus() gin.HandlerFunc {
|
||||
@@ -65,7 +65,7 @@ func RateLimitPublicStatus() gin.HandlerFunc {
|
||||
//
|
||||
// It carries no @Router annotation deliberately. openapi.json declares a
|
||||
// single server of "/api", so a @Router of /public/status/{pageId} would be
|
||||
// published as /api/public/status/{pageId} — a path that does not exist, and
|
||||
// published as /api/public/status/{pageId} - a path that does not exist, and
|
||||
// which would sit behind auth.Middleware if it did. The real address is:
|
||||
//
|
||||
// GET {scheme}://{instance-host}/public/status/{pageId}
|
||||
@@ -116,8 +116,8 @@ func getPublicStatusPage(c *gin.Context) {
|
||||
// so it is honoured only when the machine that opened the connection is one of
|
||||
// the configured trusted proxies.
|
||||
//
|
||||
// When the resulting host names no slug at all — vantage.acme.com,
|
||||
// status.acme.com, a bare IP — and the deployment is not cloud, the single
|
||||
// When the resulting host names no slug at all - vantage.acme.com,
|
||||
// status.acme.com, a bare IP - and the deployment is not cloud, the single
|
||||
// instance of that install is used. A self-hosted install has exactly one, and
|
||||
// without this every self-hosted status page 404s forever. More than one is a
|
||||
// refusal rather than a guess.
|
||||
|
||||
@@ -18,7 +18,7 @@ func runFixture() *models.WorkflowRun {
|
||||
|
||||
// A run document names every host it touched, hostname included. A restricted
|
||||
// caller must see only its own, and must be told some entries are missing
|
||||
// without being told how many — the targets_restricted precedent.
|
||||
// without being told how many - the targets_restricted precedent.
|
||||
func TestScopeRunHidesOutOfScopeServerRuns(t *testing.T) {
|
||||
got := scopeRun(runFixture(), map[string]bool{"stg-1": true}, true)
|
||||
if len(got.ServerRuns) != 1 || got.ServerRuns[0].ServerID != "stg-1" {
|
||||
@@ -45,7 +45,7 @@ func TestScopeRunLeavesUnrestrictedCallerWhole(t *testing.T) {
|
||||
}
|
||||
|
||||
// A restricted caller whose scope happens to cover the whole run must not be
|
||||
// told anything was hidden — the flag is about disclosure, not about being
|
||||
// told anything was hidden - the flag is about disclosure, not about being
|
||||
// restricted in general.
|
||||
func TestScopeRunNoFlagWhenNothingDropped(t *testing.T) {
|
||||
got := scopeRun(runFixture(), map[string]bool{"stg-1": true, "prod-1": true}, true)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// routeScopes maps a registered gin route — "<METHOD> <full path pattern>" — to
|
||||
// routeScopes maps a registered gin route - "<METHOD> <full path pattern>" - to
|
||||
// the scope an API token must hold to reach it.
|
||||
//
|
||||
// It is keyed on the route pattern rather than declared per route with a
|
||||
|
||||
@@ -40,7 +40,7 @@ func secretsReadAuth() gin.HandlerFunc {
|
||||
// esoGetGroup godoc
|
||||
//
|
||||
// @Summary Read a secret group's values (ESO)
|
||||
// @Description Consumed by Kubernetes External Secrets Operator. Authenticated with a bearer token whose SHA-256 hash is stored in settings — a different credential from an API token, never substitutable for one.
|
||||
// @Description Consumed by Kubernetes External Secrets Operator. Authenticated with a bearer token whose SHA-256 hash is stored in settings - a different credential from an API token, never substitutable for one.
|
||||
// @Tags secrets
|
||||
// @Produce json
|
||||
// @Param group path string true "Secret group name"
|
||||
|
||||
@@ -10,7 +10,7 @@ const (
|
||||
scoped scopeDecl = iota
|
||||
// fleetWide: the route deliberately reaches the whole fleet. Every
|
||||
// fleetWide entry carries a comment giving the reason. It must never mean
|
||||
// "not scoped yet" — an unresolved gap belongs on a fix list, not here,
|
||||
// "not scoped yet" - an unresolved gap belongs on a fix list, not here,
|
||||
// because this value is read as a considered decision.
|
||||
fleetWide
|
||||
// exempt: the route touches no server-derived data at all. Every exempt
|
||||
@@ -38,14 +38,14 @@ const (
|
||||
// 1. It can only ever check that a DECLARATION EXISTS, never that the handler
|
||||
// honours it. "POST /api/workflows/:id/run" was declared scoped here while
|
||||
// services.TriggerWorkflow resolved its targets through the unscoped
|
||||
// ResolveTargets — a true entry that lied, boot-enforced, for the whole
|
||||
// ResolveTargets - a true entry that lied, boot-enforced, for the whole
|
||||
// life of the feature. A declaration is a claim a reviewer must verify,
|
||||
// not a property this file establishes.
|
||||
//
|
||||
// 2. /api/mcp is exempt at route level, and that is the honest answer rather
|
||||
// than an omission. One route serves roughly twenty tools of very
|
||||
// different shapes — some read no server data at all, some resolve one
|
||||
// host, some enumerate the fleet — so no single route-level value could
|
||||
// different shapes - some read no server data at all, some resolve one
|
||||
// host, some enumerate the fleet - so no single route-level value could
|
||||
// be true of all of them. The decision genuinely lives per tool, where
|
||||
// each tool that touches server data applies auth.ServerScope's selector
|
||||
// itself, and the registry's own tests are where that is enforced.
|
||||
@@ -68,8 +68,8 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
"GET /api/servers/new": fleetWide,
|
||||
"POST /api/servers/new": fleetWide,
|
||||
|
||||
// KnownTags aggregates the tag *vocabulary* in use across the fleet — keys
|
||||
// and the values seen for them — never a server identifier or any other
|
||||
// KnownTags aggregates the tag *vocabulary* in use across the fleet - keys
|
||||
// and the values seen for them - never a server identifier or any other
|
||||
// server attribute, so it does not let a restricted token enumerate which
|
||||
// hosts exist. Filtering it would mean plumbing a selector through an
|
||||
// aggregation query for a leak that carries no server identity; ruled
|
||||
@@ -124,7 +124,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
"POST /api/vulnerabilities/rescan": fleetWide,
|
||||
|
||||
// Accepting or reopening a finding names the finding, not a server, but a
|
||||
// finding does belong to one — so a restricted token can accept a finding
|
||||
// finding does belong to one - so a restricted token can accept a finding
|
||||
// on a host outside its scope if it learns the finding ID. It cannot learn
|
||||
// one through this API any more (every listing is now scoped), so this is
|
||||
// left fleet-wide rather than given a lookup of its own. Owner|admin only.
|
||||
@@ -143,7 +143,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
// assignments whose server passes services.ServerInTokenScope before
|
||||
// returning it, so a restricted token cannot learn the hostname of an
|
||||
// out-of-scope server through a key it happens to also hold there. The
|
||||
// key document itself is still returned unfiltered — a token restricted
|
||||
// key document itself is still returned unfiltered - a token restricted
|
||||
// to staging may legitimately hold a key that is also assigned in prod,
|
||||
// and only the assignment list, not the key's existence, is the leak
|
||||
// this closes.
|
||||
@@ -152,7 +152,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
// listKeys' services.ListKeys narrows each key's AssignedCount to
|
||||
// assignments on servers ServerInTokenScope admits, for the same reason
|
||||
// as getKey above: a nonzero count on a key a restricted token sees
|
||||
// nothing assigned to in its own scope is itself the leak — it tells the
|
||||
// nothing assigned to in its own scope is itself the leak - it tells the
|
||||
// token an assignment exists on a host it must not know about, without
|
||||
// naming the host.
|
||||
"GET /api/keys": scoped,
|
||||
@@ -169,7 +169,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
"GET /api/keys/:id/private-key": exempt,
|
||||
|
||||
// Deleting a key removes it everywhere it is assigned, including on hosts
|
||||
// outside a restricted token's scope — the delete is of the key, not of a
|
||||
// outside a restricted token's scope - the delete is of the key, not of a
|
||||
// server, and there is no partial delete that leaves a key half-revoked.
|
||||
// Nothing about which hosts held it is disclosed by the call.
|
||||
"DELETE /api/keys/:id": fleetWide,
|
||||
@@ -179,7 +179,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
// listWorkflows/getWorkflow narrow Workflow.TargetServerIDs to what the
|
||||
// caller's scope admits via services.VisibleServerIDs +
|
||||
// FilterVisibleServerIDs, wrapped in WorkflowResponse so the JSON field
|
||||
// name is unchanged. TargetTags is left untouched — the tag vocabulary
|
||||
// name is unchanged. TargetTags is left untouched - the tag vocabulary
|
||||
// itself is ruled acceptable to expose, unlike a resolved server ID.
|
||||
// TargetsRestricted is set (with no count) whenever at least one target
|
||||
// was dropped.
|
||||
@@ -191,7 +191,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
// validate the ID-union-tags target set as a whole through
|
||||
// services.validateWorkflowTargetScope, which resolves the workflow's
|
||||
// targets both unscoped and scoped and refuses to save unless they match
|
||||
// — the same all-or-nothing rule the MCP create_workflow tool applies.
|
||||
// - the same all-or-nothing rule the MCP create_workflow tool applies.
|
||||
// Together these mean a restricted token can neither save a workflow
|
||||
// targeting a host or tag outside its scope (which the scheduler, firing
|
||||
// as the system, would otherwise run there) nor learn which IDs or tags
|
||||
@@ -203,12 +203,12 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
// runWorkflow passes auth.ServerScope into services.TriggerWorkflow, which
|
||||
// resolves through ResolveTargetsScoped. Note the history: this entry read
|
||||
// scoped for the whole life of the feature while TriggerWorkflow called
|
||||
// the UNSCOPED ResolveTargets — see this file's header on what this
|
||||
// the UNSCOPED ResolveTargets - see this file's header on what this
|
||||
// assertion can and cannot prove.
|
||||
"POST /api/workflows/:id/run": scoped,
|
||||
|
||||
// getRun and listWorkflowRuns narrow WorkflowRun.ServerRuns — each entry
|
||||
// of which carries a ServerID and a Hostname — to what the caller's scope
|
||||
// getRun and listWorkflowRuns narrow WorkflowRun.ServerRuns - each entry
|
||||
// of which carries a ServerID and a Hostname - to what the caller's scope
|
||||
// admits, setting servers_restricted (a boolean, never a count) when any
|
||||
// entry was dropped.
|
||||
"GET /api/runs/:runId": scoped,
|
||||
@@ -224,7 +224,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
// Deleting a workflow and cancelling a run both act on a definition rather
|
||||
// than on a server, and neither returns server data. Each can
|
||||
// nevertheless reach a definition whose targets a restricted token cannot
|
||||
// see — a cancel stops work on out-of-scope hosts. That reach is real but
|
||||
// see - a cancel stops work on out-of-scope hosts. That reach is real but
|
||||
// bounded: the caller learns nothing about which hosts are involved (both
|
||||
// /workflows listings are scoped), and a scope-narrowed variant of
|
||||
// "cancel this run" would have to either half-cancel a run or refuse one
|
||||
@@ -236,17 +236,17 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
// Arming a schedule applies no scope check of its own, and that is safe
|
||||
// only because it has nothing left to check: CreateWorkflow and
|
||||
// UpdateWorkflow (internal/services/workflows.go) already refuse to save
|
||||
// a workflow whose resolved targets — TargetServerIDs union TargetTags —
|
||||
// a workflow whose resolved targets - TargetServerIDs union TargetTags -
|
||||
// reach outside the acting credential's scope, the same all-or-nothing
|
||||
// rule the MCP create_workflow tool applies. So a workflow written after
|
||||
// this check existed had its targets constrained to whichever scope wrote
|
||||
// it, and the scheduler firing it later with a nil token scope — acting
|
||||
// as the system, not as any caller — reaches nothing that write didn't
|
||||
// it, and the scheduler firing it later with a nil token scope - acting
|
||||
// as the system, not as any caller - reaches nothing that write didn't
|
||||
// already allow.
|
||||
//
|
||||
// This holds only for workflows written after the check was added. Rows
|
||||
// already in the database were saved under the old, unvalidated rule and
|
||||
// are never re-validated — neither this route nor the writers re-check an
|
||||
// are never re-validated - neither this route nor the writers re-check an
|
||||
// existing row's targets after the fact. A workflow saved before this fix
|
||||
// with an out-of-scope tag selector still schedules and fires exactly as
|
||||
// it did before.
|
||||
@@ -270,17 +270,17 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
|
||||
// listMonitors/getMonitor redact models.Monitor.Runner to
|
||||
// models.RunnerRestricted via services.RedactMonitorRunner when it names
|
||||
// a server outside the caller's scope — Runner is literally a server ID
|
||||
// a server outside the caller's scope - Runner is literally a server ID
|
||||
// for an agent-pushed monitor, so left unfiltered it discloses one
|
||||
// directly. The monitor itself is still returned: a restricted operator
|
||||
// may legitimately need to see that it exists and is up or down, so only
|
||||
// the runner field goes neutral. Runner "server" (control-plane-run) is
|
||||
// never touched — it names no server.
|
||||
// never touched - it names no server.
|
||||
"GET /api/monitors": scoped,
|
||||
"GET /api/monitors/:id": scoped,
|
||||
|
||||
// createMonitor/updateMonitor validate the runner — which is a server ID
|
||||
// for an agent-pushed monitor — through services.validateRunner, resolving
|
||||
// createMonitor/updateMonitor validate the runner - which is a server ID
|
||||
// for an agent-pushed monitor - through services.validateRunner, resolving
|
||||
// with GetServerScoped so a restricted token can neither point a check at
|
||||
// an out-of-scope agent nor use the not-found answer as an oracle.
|
||||
"POST /api/monitors": scoped,
|
||||
@@ -293,7 +293,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
"DELETE /api/monitors/:id": fleetWide,
|
||||
|
||||
// A monitor's incidents, uptime rollups and recent samples are all about
|
||||
// the monitored endpoint — status, latency, timestamps — and carry no
|
||||
// the monitored endpoint - status, latency, timestamps - and carry no
|
||||
// server identifier at all; the runner is a field of the monitor
|
||||
// document, which these do not return.
|
||||
"GET /api/monitors/:id/incidents": exempt,
|
||||
@@ -302,7 +302,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
|
||||
// ---- notification channels ----
|
||||
|
||||
// A channel is an outbound destination — a webhook URL, an SMTP account.
|
||||
// A channel is an outbound destination - a webhook URL, an SMTP account.
|
||||
// Nothing about a server reaches these routes.
|
||||
"GET /api/channels": exempt,
|
||||
"POST /api/channels": exempt,
|
||||
@@ -328,7 +328,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
|
||||
// A status page pairs monitor IDs with per-page display names, and every
|
||||
// public read goes through services.assembleSnapshot, which is the
|
||||
// redaction boundary — its PublicComponent vocabulary has no field for a
|
||||
// redaction boundary - its PublicComponent vocabulary has no field for a
|
||||
// host, URL or runner. These authoring routes handle the page document
|
||||
// itself and never a server.
|
||||
"GET /api/status-pages": exempt,
|
||||
@@ -347,7 +347,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
// Audit rows are a record of what people and tokens did, and a row's free
|
||||
// text detail can name a host in passing ("run <id> triggered", "key
|
||||
// assigned to web-01"). Filtering the log by tag would mean parsing those
|
||||
// strings, or dropping every row whose target this token cannot resolve —
|
||||
// strings, or dropping every row whose target this token cannot resolve -
|
||||
// which would hide a restricted token's own actions from itself the
|
||||
// moment a server is renamed or deleted. The log is left whole and
|
||||
// deliberately so: an audit trail with holes in it is worth less than the
|
||||
@@ -402,7 +402,7 @@ var serverScopedRoutes = map[string]scopeDecl{
|
||||
|
||||
// AssertServerScopeMapComplete refuses to boot when any registered /api route
|
||||
// is missing from serverScopedRoutes. routes is every /api route the engine
|
||||
// registered — not a filtered subset — which is the whole point of the
|
||||
// registered - not a filtered subset - which is the whole point of the
|
||||
// inversion: a new route is checked by default rather than only when its path
|
||||
// happens to match a pattern somebody remembered to add.
|
||||
func AssertServerScopeMapComplete(routes []string) error {
|
||||
|
||||
@@ -32,7 +32,7 @@ func registerStatusPageRoutes(g *gin.RouterGroup) {
|
||||
|
||||
// statusPageError maps the service errors onto codes once, so ten handlers do
|
||||
// not each invent their own. services.ErrPageInvalid covers every validation
|
||||
// failure in the status page and incident services — a missing title or an
|
||||
// failure in the status page and incident services - a missing title or an
|
||||
// invalid incident status is a 400, not a 500.
|
||||
func statusPageError(c *gin.Context, err error) {
|
||||
switch {
|
||||
|
||||
@@ -87,7 +87,7 @@ func createToken(c *gin.Context) {
|
||||
|
||||
// A token-authenticated request may only mint a token whose scopes are a
|
||||
// subset of its own. Role is capped against the creating *user* below (in
|
||||
// services.CreateAPIToken), but a role cap alone does not confine scopes —
|
||||
// services.CreateAPIToken), but a role cap alone does not confine scopes -
|
||||
// without this, a CI token holding only settings:write could mint a token
|
||||
// holding keys:write and secrets:write, since minting only ever required
|
||||
// settings:write and never checked what the caller itself could reach. A
|
||||
|
||||
@@ -24,7 +24,7 @@ type LimitExceededResponse struct {
|
||||
}
|
||||
|
||||
// LicenceErrorResponse pairs an error with a machine-readable reason rather
|
||||
// than a code — used only on the two licence rejection paths that predate the
|
||||
// than a code - used only on the two licence rejection paths that predate the
|
||||
// error/code convention used everywhere else.
|
||||
type LicenceErrorResponse struct {
|
||||
Error string `json:"error"`
|
||||
@@ -115,7 +115,7 @@ type AgentVersionResponse struct {
|
||||
}
|
||||
|
||||
// WorkflowResponse is a workflow with its TargetServerIDs narrowed to what
|
||||
// the acting token's scope admits — the explicit field shadows the embedded
|
||||
// the acting token's scope admits - the explicit field shadows the embedded
|
||||
// one for JSON marshalling, matching the pattern KeyDetailResponse already
|
||||
// uses. TargetTags is not filtered: the tag vocabulary itself is ruled
|
||||
// acceptable to expose, and only the resolved ID list can name a specific
|
||||
@@ -134,7 +134,7 @@ type WorkflowResponse struct {
|
||||
// RunResponse is a workflow run with its ServerRuns narrowed to the servers
|
||||
// the acting token's scope admits. Each models.ServerRun carries both a
|
||||
// ServerID and a Hostname, so an unfiltered run document names every host it
|
||||
// touched — the same disclosure WorkflowResponse.TargetServerIDs closes one
|
||||
// touched - the same disclosure WorkflowResponse.TargetServerIDs closes one
|
||||
// level up, and the parent of the per-server log routes that were already
|
||||
// scoped.
|
||||
//
|
||||
|
||||
@@ -29,7 +29,7 @@ type vulnGroup struct {
|
||||
// listVulnerabilities godoc
|
||||
//
|
||||
// @Summary List vulnerabilities
|
||||
// @Description Groups findings by CVE, most severe first — the same CVE on forty servers is one decision, not forty rows.
|
||||
// @Description Groups findings by CVE, most severe first - the same CVE on forty servers is one decision, not forty rows.
|
||||
// @Tags vulnerabilities
|
||||
// @Produce json
|
||||
// @Param severity query string false "Filter by severity"
|
||||
@@ -102,7 +102,7 @@ func groupByCVE(findings []models.VulnFinding) []vulnGroup {
|
||||
}
|
||||
|
||||
// hasFixFromQuery reads ?has_fix=true|false. Anything else, including an empty
|
||||
// or malformed value, is no filter — a filter nobody asked for must never hide
|
||||
// or malformed value, is no filter - a filter nobody asked for must never hide
|
||||
// findings, and the wrong direction here hides the unfixable ones.
|
||||
func hasFixFromQuery(c *gin.Context) *bool {
|
||||
switch c.Query("has_fix") {
|
||||
@@ -199,7 +199,7 @@ type acceptFindingRequest struct {
|
||||
// acceptFinding godoc
|
||||
//
|
||||
// @Summary Accept a finding
|
||||
// @Description Requires a reason and a future expiry. Reopens automatically at expiry — permanent dismissal is never allowed.
|
||||
// @Description Requires a reason and a future expiry. Reopens automatically at expiry - permanent dismissal is never allowed.
|
||||
// @Tags vulnerabilities
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
@@ -221,7 +221,7 @@ func acceptFinding(c *gin.Context) {
|
||||
|
||||
// Both rejected deliberately. An acceptance with no reason is a dismissal
|
||||
// nobody can audit, and one already expired is a permanent dismissal
|
||||
// wearing an expiry — the graveyard the expiry exists to prevent.
|
||||
// wearing an expiry - the graveyard the expiry exists to prevent.
|
||||
if strings.TrimSpace(req.Reason) == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "a reason is required"})
|
||||
return
|
||||
@@ -313,7 +313,7 @@ func listServerVulnerabilities(c *gin.Context) {
|
||||
// getServerPackages godoc
|
||||
//
|
||||
// @Summary Get a server's package inventory
|
||||
// @Description A server that has not reported yet answers reported=false rather than 404 — that is the normal state for the first hour after install.
|
||||
// @Description A server that has not reported yet answers reported=false rather than 404 - that is the normal state for the first hour after install.
|
||||
// @Tags vulnerabilities
|
||||
// @Produce json
|
||||
// @Param id path string true "Server ID"
|
||||
|
||||
@@ -151,7 +151,7 @@ func controlWorkload(c *gin.Context) {
|
||||
case errors.Is(err, services.ErrAgentNotConnected):
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": err.Error()})
|
||||
case services.IsWorkloadProtected(err):
|
||||
// Nothing failed — the agent refused, which is the design. 409, not
|
||||
// Nothing failed - the agent refused, which is the design. 409, not
|
||||
// 500, and the reason is carried through.
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
default:
|
||||
|
||||
@@ -44,7 +44,7 @@ func githubOAuthConfig(p *models.AuthProvider, secret, redirectURL string) *oaut
|
||||
//
|
||||
// Verified alone is not enough: a non-primary address is one the person happens
|
||||
// to have proved, not the one they present as themselves. Primary alone is far
|
||||
// worse — an unverified address is not proof of control at all, and accepting
|
||||
// worse - an unverified address is not proof of control at all, and accepting
|
||||
// one would let anyone with a GitHub account claim any address in the instance.
|
||||
func selectGitHubEmail(emails []githubEmail) (string, error) {
|
||||
for _, e := range emails {
|
||||
|
||||
@@ -61,8 +61,8 @@ func hostSlug(host string) string {
|
||||
func HostSlug(host string) string { return hostSlug(host) }
|
||||
|
||||
// InstanceFromHost resolves the instance named by the request's own Host
|
||||
// header. Callers that must resolve a host from somewhere else — the public
|
||||
// status page reads a trusted X-Forwarded-Host — use InstanceForHost so the
|
||||
// header. Callers that must resolve a host from somewhere else - the public
|
||||
// status page reads a trusted X-Forwarded-Host - use InstanceForHost so the
|
||||
// slug rules and the 60s cache stay single-implementation.
|
||||
func InstanceFromHost(c *gin.Context) (*models.Instance, bool) {
|
||||
return InstanceForHost(c.Request.Host)
|
||||
@@ -82,7 +82,7 @@ func InstanceForHost(host string) (*models.Instance, bool) {
|
||||
if err != nil || inst == nil {
|
||||
// Negative entries are cached too. Without them an unknown but
|
||||
// well-formed host costs a Mongo query per anonymous request, which
|
||||
// the public status page exposes to the open internet — and the
|
||||
// the public status page exposes to the open internet - and the
|
||||
// round trip is itself a timing oracle separating "no such instance"
|
||||
// from "instance exists, page does not".
|
||||
storeInstance(slug, nil)
|
||||
@@ -93,7 +93,7 @@ func InstanceForHost(host string) (*models.Instance, bool) {
|
||||
}
|
||||
|
||||
// SoleInstance resolves the one instance of a deployment that has exactly one.
|
||||
// It is how a self-hosted install serves a host that names no slug at all —
|
||||
// It is how a self-hosted install serves a host that names no slug at all -
|
||||
// vantage.acme.com, status.acme.com, or a bare address. It reuses the same
|
||||
// count-then-read that bootstrap uses, and refuses rather than guessing when
|
||||
// more than one instance exists.
|
||||
|
||||
@@ -89,7 +89,7 @@ func HandleLocalLogin(c *gin.Context) {
|
||||
}
|
||||
|
||||
// HandleListPublicProviders is unauthenticated: it is what the login page reads
|
||||
// to decide what to draw. It carries no issuer, no client ID and no secret —
|
||||
// to decide what to draw. It carries no issuer, no client ID and no secret -
|
||||
// only what a button needs, because anyone who can reach the login page can
|
||||
// read this.
|
||||
func HandleListPublicProviders(c *gin.Context) {
|
||||
|
||||
@@ -104,7 +104,7 @@ func sessionFromCookie(c *gin.Context) (*Session, bool) {
|
||||
}
|
||||
sess, err := GetSession(c.Request.Context(), cookie.Value)
|
||||
if err != nil {
|
||||
// A stale cookie plus a valid bearer token is a real combination —
|
||||
// A stale cookie plus a valid bearer token is a real combination -
|
||||
// a browser tab left open beside a curl. Fall through rather than
|
||||
// refusing a credential that would have worked.
|
||||
if bearerToken(c) != "" {
|
||||
@@ -203,7 +203,7 @@ func IsToken(c *gin.Context) bool { return TokenID(c) != "" }
|
||||
|
||||
// ServerScope is the tag restriction the acting credential carries, or nil for
|
||||
// an unrestricted token and for every cookie session. Callers pass it to
|
||||
// services.ServerInTokenScope or services.IntersectSelectors — nil means the
|
||||
// services.ServerInTokenScope or services.IntersectSelectors - nil means the
|
||||
// whole fleet, never nothing.
|
||||
func ServerScope(c *gin.Context) map[string]string {
|
||||
if s := GetSessionFromContext(c); s != nil {
|
||||
|
||||
@@ -44,7 +44,7 @@ var presets = []Preset{
|
||||
Kind: models.KindOIDC,
|
||||
IssuerFormat: "https://%s/oauth2/default",
|
||||
InputLabel: "Okta org domain",
|
||||
InputHint: "e.g. acme.okta.com — no scheme, no trailing slash.",
|
||||
InputHint: "e.g. acme.okta.com - no scheme, no trailing slash.",
|
||||
Scopes: []string{"openid", "profile", "email"},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ type Session struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
// The four fields below are set only when the request authenticated with
|
||||
// an API token. They are never persisted to Redis — a token authenticates
|
||||
// an API token. They are never persisted to Redis - a token authenticates
|
||||
// per request and mints no session, so a revoked token stops working
|
||||
// immediately rather than at the end of a session TTL.
|
||||
TokenID string `json:"-"`
|
||||
@@ -38,7 +38,7 @@ var rdb *redis.Client
|
||||
// InitRedis connects the session store.
|
||||
//
|
||||
// Username and password may both be empty for an unauthenticated instance. For
|
||||
// a legacy `requirepass` Redis, pass the password with an empty username —
|
||||
// a legacy `requirepass` Redis, pass the password with an empty username -
|
||||
// go-redis then sends AUTH with one argument instead of two.
|
||||
func InitRedis(addr, username, password string) error {
|
||||
rdb = redis.NewClient(&redis.Options{
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
//
|
||||
// Everything here is deliberately best-effort delivery with an explicit ack
|
||||
// rather than a queue. A command whose owner pod died between the presence
|
||||
// check and the publish must fail loudly and immediately — the caller answers
|
||||
// 503 and the operator retries — not sit in a queue waiting for a stream that
|
||||
// check and the publish must fail loudly and immediately - the caller answers
|
||||
// 503 and the operator retries - not sit in a queue waiting for a stream that
|
||||
// no longer exists.
|
||||
package bus
|
||||
|
||||
@@ -97,7 +97,7 @@ const (
|
||||
//
|
||||
// The listener cannot be bound in advance on any particular pod. An agent's
|
||||
// ProxyStream is a separate HTTP/2 request from its CommandStream, and an
|
||||
// L7 proxy (Traefik) balances requests, not connections — so it may land on
|
||||
// L7 proxy (Traefik) balances requests, not connections - so it may land on
|
||||
// any replica, not the one holding the command stream. The pod it does land
|
||||
// on binds the listener and announces it here.
|
||||
ProxyAddrChannel = prefix + "proxyaddr:"
|
||||
@@ -295,7 +295,7 @@ func SetPendingProxy(ctx context.Context, proxyID, instanceID, serverID string,
|
||||
// round trips: single use is the whole security property, and two agents
|
||||
// racing the same proxy_id must not both be served.
|
||||
//
|
||||
// A missing record is reported as "", "" rather than an error — an unknown
|
||||
// A missing record is reported as "", "" rather than an error - an unknown
|
||||
// proxy_id, an expired one and a second claim are all the same refusal.
|
||||
func ClaimPendingProxy(ctx context.Context, proxyID string) (instanceID, serverID string) {
|
||||
v, err := claimPending.Run(ctx, rdb, []string{ProxyPendingKey + proxyID}).Text()
|
||||
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
// a race anyone wins.
|
||||
//
|
||||
// Redis rather than a Kubernetes Lease so that Docker Compose, which has no
|
||||
// API server, takes the identical code path — one implementation to reason
|
||||
// API server, takes the identical code path - one implementation to reason
|
||||
// about, not two.
|
||||
//
|
||||
// job is given a context cancelled the moment leadership is lost, and must
|
||||
|
||||
@@ -45,7 +45,7 @@ func (s *vantageServer) ProxyStream(stream pb.Vantage_ProxyStreamServer) error {
|
||||
// testable without a real stream.
|
||||
//
|
||||
// The listener is bound here, on whichever replica the stream reached, rather
|
||||
// than in advance on the pod holding the agent's command stream — those are not
|
||||
// than in advance on the pod holding the agent's command stream - those are not
|
||||
// the same pod, because an L7 proxy balances HTTP/2 requests independently.
|
||||
func serveProxy(open *pb.ProxyOpen, instanceID string, stream proxy.AgentStream) error {
|
||||
sess, err := services.ClaimProxyStream(instanceID, open.ServerId, open.ProxyId)
|
||||
|
||||
@@ -168,7 +168,7 @@ func (s *vantageServer) ReportPackages(ctx context.Context, req *pb.ReportPackag
|
||||
//
|
||||
// It is not gated by licence: the workload registry reads as core fleet
|
||||
// management rather than a premium add-on. If that ever changes, the check
|
||||
// belongs here — gating collection, not display — for the same reason it does
|
||||
// belongs here - gating collection, not display - for the same reason it does
|
||||
// in ReportPackages.
|
||||
func (s *vantageServer) ReportWorkloads(ctx context.Context, req *pb.ReportWorkloadsRequest) (*pb.ReportWorkloadsResponse, error) {
|
||||
srv, err := services.ValidateAgentToken(req.ServerId, req.AgentToken)
|
||||
@@ -304,7 +304,7 @@ func (s *vantageServer) CommandStream(stream pb.Vantage_CommandStreamServer) err
|
||||
|
||||
// Serve claims this agent's presence on the bus and subscribes this pod to
|
||||
// its command channel, so a dispatch issued by any other replica arrives
|
||||
// here. The teardown releases both — an agent that reconnects to a
|
||||
// here. The teardown releases both - an agent that reconnects to a
|
||||
// different pod must not leave this one advertising a stream it no longer
|
||||
// has.
|
||||
ch, release := services.Dispatcher.Serve(stream.Context(), srv.ServerID)
|
||||
@@ -422,7 +422,7 @@ const pingSummaryInterval = 5 * time.Minute
|
||||
// presence claim, released by a deferred call that a killed process never runs.
|
||||
// The claim then outlives its owner for the remainder of its 30s TTL, during
|
||||
// which dispatch believes the agent is reachable, publishes to a channel with
|
||||
// no subscriber, and fails as "agent offline" — a pod that has already exited
|
||||
// no subscriber, and fails as "agent offline" - a pod that has already exited
|
||||
// still answering for an agent it can no longer reach.
|
||||
func StartGRPC(port int) (stop func(), err error) {
|
||||
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
|
||||
@@ -458,7 +458,7 @@ func StartGRPC(port int) (stop func(), err error) {
|
||||
//
|
||||
// It is bounded: an idle CommandStream returns as soon as its context is
|
||||
// cancelled, but a console relay mid-transfer would otherwise hold the
|
||||
// process past the pod's grace period and earn a SIGKILL — which is the
|
||||
// process past the pod's grace period and earn a SIGKILL - which is the
|
||||
// abrupt exit this exists to avoid.
|
||||
return func() {
|
||||
done := make(chan struct{})
|
||||
|
||||
@@ -35,7 +35,7 @@ var ErrConfirmRequired = errors.New("confirmation required")
|
||||
var ErrOutOfScope = errors.New("targets outside token scope")
|
||||
|
||||
// logEvent is services.LogEvent behind a package variable so tests can
|
||||
// observe what would have been audited without a live database connection —
|
||||
// observe what would have been audited without a live database connection -
|
||||
// services.LogEvent talks straight to Mongo via db.Col, which panics on a nil
|
||||
// client outside a real boot.
|
||||
var logEvent = services.LogEvent
|
||||
@@ -117,7 +117,7 @@ func LogDenied(c Caller, toolName, gate string) {
|
||||
}
|
||||
|
||||
// LogFailure records a write tool call that reached a service and that
|
||||
// service returned an error — as opposed to LogDenied, which records a
|
||||
// service returned an error - as opposed to LogDenied, which records a
|
||||
// policy refusal that never reached one. Distinguishing the two in
|
||||
// audit_logs is what lets a human reading it tell "the agent was stopped"
|
||||
// from "the agent tried and the machine failed".
|
||||
@@ -130,7 +130,7 @@ func LogFailure(c Caller, t Tool, args map[string]any, err error) {
|
||||
//
|
||||
// It is a distinct event type rather than another mcp.tool_call row because of
|
||||
// the question a human will actually ask, which is "what has this agent added
|
||||
// to my instance" — an answer buried among hundreds of read rows is not an
|
||||
// to my instance" - an answer buried among hundreds of read rows is not an
|
||||
// answer.
|
||||
func LogCreated(c Caller, kind, id, name string) {
|
||||
logEvent(c.InstanceID, "mcp.created", c.TokenName, "", "",
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
// It is a presentation layer over the service layer and introduces no authority
|
||||
// of its own: every tool calls the same service functions the REST handlers
|
||||
// call, and every decision about who may do what is made by machinery that
|
||||
// already exists. Three gates apply to every call — the licence feature, the
|
||||
// mcp:* scope, and the tool's own resource scope — and all three must pass.
|
||||
// already exists. Three gates apply to every call - the licence feature, the
|
||||
// mcp:* scope, and the tool's own resource scope - and all three must pass.
|
||||
package mcp
|
||||
|
||||
import (
|
||||
@@ -50,7 +50,7 @@ const (
|
||||
// what tagArg decodes.
|
||||
ArgTagMap ArgType = "tag_map"
|
||||
// ArgObject is a free-form object whose inner shape the tool documents in
|
||||
// the argument description — create_monitor's target, whose fields differ
|
||||
// the argument description - create_monitor's target, whose fields differ
|
||||
// per monitor type.
|
||||
ArgObject ArgType = "object"
|
||||
)
|
||||
@@ -83,7 +83,7 @@ type Tool struct {
|
||||
Write bool
|
||||
// Args declares every argument the handler reads, in the order a client
|
||||
// should see them. A tool taking none declares an empty slice, which is
|
||||
// distinct from "nobody has written the schema yet" — see the registry
|
||||
// distinct from "nobody has written the schema yet" - see the registry
|
||||
// tests, which require the declaration to be deliberate.
|
||||
Args []ToolArg
|
||||
// TouchesServers marks a tool that returns or acts on server-derived data:
|
||||
@@ -92,7 +92,7 @@ type Tool struct {
|
||||
// ResolveTargetsScoped, ListServersFiltered or VisibleServerIDs.
|
||||
//
|
||||
// Like serverScopedRoutes in the api package, this can only ever assert
|
||||
// that a declaration exists, never that the handler honours it — get_run_logs
|
||||
// that a declaration exists, never that the handler honours it - get_run_logs
|
||||
// proved the run's instance and the server's membership in the run and then
|
||||
// read production stdout for a staging token. What it does buy is that
|
||||
// adding a tool forces an answer to "does this touch server data?", and the
|
||||
|
||||
@@ -72,8 +72,8 @@ func TestInputSchemaMarshals(t *testing.T) {
|
||||
// serverTouchingTools names every tool that returns or acts on server-derived
|
||||
// data. The test below pins the registry against it, so a tool added that
|
||||
// reads a hostname, a server ID, a package list or a run's per-server output
|
||||
// fails until somebody declares TouchesServers and — the point of the exercise
|
||||
// — decides how it applies Caller.TokenScope.
|
||||
// fails until somebody declares TouchesServers and - the point of the exercise
|
||||
// - decides how it applies Caller.TokenScope.
|
||||
//
|
||||
// This is the assertion that would have caught get_run_logs, which proved the
|
||||
// run's instance and the named server's membership in the run and then read
|
||||
|
||||
@@ -141,7 +141,7 @@ func buildMonitor(args map[string]any) (models.Monitor, error) {
|
||||
// named agent, and a silently ignored argument would leave a model
|
||||
// believing it had. services.CreateMonitor now validates a runner
|
||||
// through GetServerScoped as well, so this is a second line rather than
|
||||
// the only one — but the clearer answer belongs here.
|
||||
// the only one - but the clearer answer belongs here.
|
||||
if _, present := args["runner"]; present {
|
||||
return models.Monitor{}, fmt.Errorf("runner cannot be set from here; monitors created this way always run on the control plane")
|
||||
}
|
||||
@@ -176,7 +176,7 @@ func init() {
|
||||
Write: true,
|
||||
Scope: "workflows:write",
|
||||
Description: "Create a reusable workflow step: a named script with an interpreter. " +
|
||||
"The step is SAVED to this Vantage instance but is not run by creating it — " +
|
||||
"The step is SAVED to this Vantage instance but is not run by creating it - " +
|
||||
"add it to a workflow with create_workflow, then run that with run_workflow. " +
|
||||
"Steps created this way cannot reference secrets.",
|
||||
Handler: func(ctx context.Context, c Caller, args map[string]any) (any, error) {
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
// Runner in particular is not merely omitted as noise: for an agent-pushed
|
||||
// monitor it is literally a server ID, and REST's listMonitors/getMonitor
|
||||
// redact it to models.RunnerRestricted when that server is outside the
|
||||
// caller's scope. This projection never had a runner field to redact — the
|
||||
// caller's scope. This projection never had a runner field to redact - the
|
||||
// same outcome, reached by never including it rather than by filtering it
|
||||
// out, so this tool and get_monitor_status cannot disagree with the REST
|
||||
// surface about what a restricted token learns.
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestServerSummaryStaysSmall(t *testing.T) {
|
||||
}
|
||||
|
||||
// The real status vocabulary is "pending" / "active" / "offline" (see
|
||||
// internal/services/servers.go) — "online" is never assigned anywhere. A
|
||||
// internal/services/servers.go) - "online" is never assigned anywhere. A
|
||||
// server carrying the live status ("active") must project as Online: true,
|
||||
// or list_servers/get_server misreport the entire fleet as down.
|
||||
func TestSummariseServerReportsActiveAsOnline(t *testing.T) {
|
||||
|
||||
@@ -39,7 +39,7 @@ type workflowDetail struct {
|
||||
Tags map[string]string `json:"target_tags,omitempty"`
|
||||
Schedule string `json:"schedule,omitempty"`
|
||||
// TargetsRestricted is set, with no count, when Targets omits at least
|
||||
// one server ID outside this token's scope — mirroring
|
||||
// one server ID outside this token's scope - mirroring
|
||||
// WorkflowResponse's REST field, so a model reading this alongside a
|
||||
// run_workflow refusal for the same workflow is not left to conclude the
|
||||
// refusal invented a problem this tool never mentioned.
|
||||
@@ -341,7 +341,7 @@ func init() {
|
||||
//
|
||||
// The refusal reuses the membership message verbatim so that
|
||||
// "in the run but out of your scope" and "not in the run at all"
|
||||
// are indistinguishable — otherwise the difference between the
|
||||
// are indistinguishable - otherwise the difference between the
|
||||
// two answers enumerates hosts the token cannot see.
|
||||
if _, err := services.GetServerScoped(c.InstanceID, serverID, c.TokenScope); err != nil {
|
||||
return nil, fmt.Errorf("server %q is not part of run %q", serverID, runID)
|
||||
@@ -461,7 +461,7 @@ func init() {
|
||||
}
|
||||
|
||||
// Go randomises map iteration order, so truncating a ranged map
|
||||
// to a page made two identical calls return different CVEs — a
|
||||
// to a page made two identical calls return different CVEs - a
|
||||
// model comparing its own two answers would see the fleet change
|
||||
// under it. Sorting by CVE ID (then package, since the key is a
|
||||
// pair) makes the page deterministic.
|
||||
@@ -541,7 +541,7 @@ func init() {
|
||||
},
|
||||
TouchesServers: true,
|
||||
Scope: "vulns:read",
|
||||
Description: "Search every server's installed packages by name across the whole fleet — " +
|
||||
Description: "Search every server's installed packages by name across the whole fleet - " +
|
||||
"answers questions like \"which hosts still run OpenSSL 1.1\". version_below is not " +
|
||||
"currently supported: filtering package versions correctly requires knowing each " +
|
||||
"distribution's own version-ordering scheme (dpkg/rpm/apk), which this tool cannot " +
|
||||
@@ -555,7 +555,7 @@ func init() {
|
||||
if stringArg(args, "version_below") != "" {
|
||||
return nil, fmt.Errorf("version_below is not supported: correct version ordering " +
|
||||
"depends on each host's distribution (dpkg/rpm/apk each order differently), " +
|
||||
"which this tool cannot resolve here — omit version_below and every matching " +
|
||||
"which this tool cannot resolve here - omit version_below and every matching " +
|
||||
"install is returned instead")
|
||||
}
|
||||
|
||||
@@ -616,7 +616,7 @@ func init() {
|
||||
// This tool reads no arguments at all.
|
||||
Args: []ToolArg{},
|
||||
Scope: "secrets:read",
|
||||
Description: "List secret group and key names on this instance. Metadata only — no " +
|
||||
Description: "List secret group and key names on this instance. Metadata only - no " +
|
||||
"tool ever returns a secret's plaintext value to a model.",
|
||||
Handler: func(ctx context.Context, c Caller, args map[string]any) (any, error) {
|
||||
groups, err := services.ListSecretGroups(c.InstanceID)
|
||||
|
||||
@@ -43,8 +43,8 @@ type runStartedResult struct {
|
||||
// workflow's own configured target_server_ids/target_tags via
|
||||
// services.ResolveTargets (unscoped) and runs against exactly that set. There
|
||||
// is no per-call server_ids/tags override to plumb through, so this tool takes
|
||||
// only workflow_id. To keep the token's scope meaningful — TriggerWorkflow
|
||||
// itself does not consult it — this handler first loads the workflow and
|
||||
// only workflow_id. To keep the token's scope meaningful - TriggerWorkflow
|
||||
// itself does not consult it - this handler first loads the workflow and
|
||||
// resolves its configured targets through ResolveTargetsScoped with the
|
||||
// caller's TokenScope, and refuses the run outright if that scoped view does
|
||||
// not cover every server the unscoped resolution would touch. That is the
|
||||
@@ -61,7 +61,7 @@ func init() {
|
||||
Write: true,
|
||||
Scope: "workflows:write",
|
||||
Description: "Run a workflow against the servers it is already configured to target " +
|
||||
"(its saved server list and tags — this call does not let you pick different " +
|
||||
"(its saved server list and tags - this call does not let you pick different " +
|
||||
"targets). This EXECUTES COMMANDS on real machines and cannot be undone from " +
|
||||
"here. Returns a run ID immediately; poll get_run for progress and get_run_logs " +
|
||||
"for output. Refused if the workflow's targets reach outside this token's own " +
|
||||
@@ -121,7 +121,7 @@ type cancelledResult struct {
|
||||
// cancel_run. The REST cancel route (workflows.go's cancelRun) calls
|
||||
// services.CancelRun(instanceID, runID) directly; that call is already scoped
|
||||
// to the caller's instance by instanceID, which is what "verifies the run
|
||||
// belongs to the caller's instance" reduces to here — there is no separate
|
||||
// belongs to the caller's instance" reduces to here - there is no separate
|
||||
// per-server scope to check, since cancelling touches the run record, not a
|
||||
// server.
|
||||
func init() {
|
||||
@@ -164,7 +164,7 @@ type updateBatchResult struct {
|
||||
// There is no fleet-wide variant of that service call to invoke once, so this
|
||||
// tool resolves the requested targets through ResolveTargetsScoped exactly as
|
||||
// the brief describes, then calls the same DispatchApplyUpdates the REST route
|
||||
// calls, once per resolved server — the identical dispatch, just looped
|
||||
// calls, once per resolved server - the identical dispatch, just looped
|
||||
// instead of hardcoded to one server_id from the URL.
|
||||
func init() {
|
||||
All().Register(Tool{
|
||||
@@ -275,12 +275,12 @@ type assignKeyResult struct {
|
||||
|
||||
// assign_key. The REST route (handlers.go's assignKey) takes one server_id in
|
||||
// the body and calls services.AssignKey(instanceID, keyID, serverID) directly
|
||||
// — AssignKey itself resolves the server with the unscoped services.GetServer,
|
||||
// - AssignKey itself resolves the server with the unscoped services.GetServer,
|
||||
// not GetServerScoped, so the REST route carries no token-scope check of its
|
||||
// own (session auth has no server-scope restriction; only API tokens do). For
|
||||
// the MCP surface, this tool resolves every named target through
|
||||
// ResolveTargetsScoped first — the same chokepoint every other target-
|
||||
// resolving write tool goes through — so a token whose scope excludes a server
|
||||
// ResolveTargetsScoped first - the same chokepoint every other target-
|
||||
// resolving write tool goes through - so a token whose scope excludes a server
|
||||
// cannot reach it here even though the REST handler's own server lookup would
|
||||
// not have stopped it. Then it calls the identical AssignKey once per resolved
|
||||
// server.
|
||||
@@ -298,7 +298,7 @@ func init() {
|
||||
Scope: "keys:write",
|
||||
Description: "Assign an SSH key to real servers, selected by server_ids and/or " +
|
||||
"tags. The agent rewrites /root/.ssh/authorized_keys on each targeted machine " +
|
||||
"and this cannot be undone from here — use revoke to remove it afterward.",
|
||||
"and this cannot be undone from here - use revoke to remove it afterward.",
|
||||
Handler: func(ctx context.Context, c Caller, args map[string]any) (any, error) {
|
||||
keyID := stringArg(args, "key_id")
|
||||
if keyID == "" {
|
||||
|
||||
@@ -31,7 +31,7 @@ func callerFromContext(c *gin.Context) Caller {
|
||||
// because a stateless server has no session to open the server-to-client SSE
|
||||
// stream against. The GET route is still registered deliberately (see
|
||||
// handlers.go) so a client probing for the endpoint sees a protocol-correct
|
||||
// 405 rather than gin's 404 — the MCP spec expects exactly that response from
|
||||
// 405 rather than gin's 404 - the MCP spec expects exactly that response from
|
||||
// a server that does not offer the GET/SSE leg. Nothing here should route GET
|
||||
// requests differently or try to make them do anything else.
|
||||
func Handler() gin.HandlerFunc {
|
||||
@@ -69,7 +69,7 @@ func Handler() gin.HandlerFunc {
|
||||
func registerSDKTool(srv *sdk.Server, tool Tool, caller Caller) {
|
||||
// InputSchema is set explicitly rather than inferred from the handler's
|
||||
// argument type. The SDK can infer one from a typed In parameter, which is
|
||||
// cleaner where it fits — but every ToolFunc here takes map[string]any, and
|
||||
// cleaner where it fits - but every ToolFunc here takes map[string]any, and
|
||||
// inference over that yields a bare open object saying nothing. Giving each
|
||||
// tool its own Go argument struct would mean twenty-odd structs and a
|
||||
// generic registry that could no longer hold them in one map, losing the
|
||||
@@ -86,8 +86,8 @@ func registerSDKTool(srv *sdk.Server, tool Tool, caller Caller) {
|
||||
}
|
||||
|
||||
// callTool is the gate check, dispatch and audit write registerSDKTool wraps
|
||||
// onto the SDK's call signature. It is a separate function — rather than the
|
||||
// closure body inline — so it can be exercised directly in tests without
|
||||
// onto the SDK's call signature. It is a separate function - rather than the
|
||||
// closure body inline - so it can be exercised directly in tests without
|
||||
// standing up an sdk.Server and driving a real MCP request through it.
|
||||
func callTool(ctx context.Context, tool Tool, caller Caller, args map[string]any) (*sdk.CallToolResult, any, error) {
|
||||
if ok, gate := Allowed(tool, caller); !ok {
|
||||
@@ -98,14 +98,14 @@ func callTool(ctx context.Context, tool Tool, caller Caller, args map[string]any
|
||||
out, err := tool.Handler(ctx, caller, args)
|
||||
if err != nil {
|
||||
// A write tool's own handler never gets a chance to audit its own
|
||||
// refusal or failure — it returns before reaching its LogCall, and
|
||||
// refusal or failure - it returns before reaching its LogCall, and
|
||||
// unlike a successful write, this layer does not know a resolved
|
||||
// server count to pass along anyway. So every write failure is
|
||||
// audited here instead: a policy refusal (fan-out or tag scope) as
|
||||
// mcp.tool_denied naming the gate, everything else as
|
||||
// mcp.tool_failed, so a human reading audit_logs can tell "the agent
|
||||
// was stopped" from "the agent tried and the machine failed". Read
|
||||
// tools are unaffected — a failed read was never going to change
|
||||
// tools are unaffected - a failed read was never going to change
|
||||
// anything and carries no gate to name.
|
||||
if tool.Write {
|
||||
switch {
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
// TestRefusedWriteIsAudited exercises the real dispatch path (callTool, which
|
||||
// registerSDKTool wraps) for a write tool whose handler refuses the call
|
||||
// before it ever reaches its own LogCall — a fan-out refusal, in this case,
|
||||
// before it ever reaches its own LogCall - a fan-out refusal, in this case,
|
||||
// which run_workflow, apply_updates, update_agent and assign_key all reach
|
||||
// the same way via CheckFanOut. The refusal must still produce an audit row:
|
||||
// a blocked mutation attempt is the single most audit-worthy event a write
|
||||
@@ -77,8 +77,8 @@ func TestOutOfScopeWriteIsAudited(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestServiceFailureIsAuditedDistinctly makes sure a write tool failing for a
|
||||
// reason that is not a policy refusal — the underlying service call itself
|
||||
// erroring — is still audited, but as mcp.tool_failed rather than
|
||||
// reason that is not a policy refusal - the underlying service call itself
|
||||
// erroring - is still audited, but as mcp.tool_failed rather than
|
||||
// mcp.tool_denied, so a human reading audit_logs can tell the two apart.
|
||||
func TestServiceFailureIsAuditedDistinctly(t *testing.T) {
|
||||
var events []string
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
//
|
||||
// The plaintext is shown once at creation and never stored: only TokenHash,
|
||||
// which is sha256 hex of the value, exactly as servers.agent_token_hash and the
|
||||
// ESO read token already are. bcrypt is deliberately not used — the value is
|
||||
// ESO read token already are. bcrypt is deliberately not used - the value is
|
||||
// full-entropy random rather than a chosen password, and a per-token salt would
|
||||
// force a collection scan where an indexed lookup is wanted.
|
||||
//
|
||||
|
||||
@@ -22,7 +22,7 @@ const RedactedSecret = "••••••••"
|
||||
// channelSecretKeys names, per channel type, the config entries that are
|
||||
// credentials rather than settings. A Slack or Discord webhook URL is on this
|
||||
// list because possession of the URL *is* the authorisation to post to that
|
||||
// channel — there is nothing else to steal.
|
||||
// channel - there is nothing else to steal.
|
||||
var channelSecretKeys = map[string][]string{
|
||||
ChannelWebhook: {"url"},
|
||||
ChannelSlack: {"url"},
|
||||
|
||||
@@ -23,8 +23,8 @@ const RunnerServer = "server"
|
||||
|
||||
// RunnerRestricted replaces a monitor's runner in an API response when the
|
||||
// real value is a server ID the acting token's scope does not admit. The
|
||||
// monitor itself is still returned — a restricted operator may legitimately
|
||||
// need to see its name and state — only where it runs is hidden, the same
|
||||
// monitor itself is still returned - a restricted operator may legitimately
|
||||
// need to see its name and state - only where it runs is hidden, the same
|
||||
// way a workflow's target list can omit an ID without the whole workflow
|
||||
// disappearing from a list.
|
||||
const RunnerRestricted = "restricted"
|
||||
|
||||
@@ -63,7 +63,7 @@ type VulnFinding struct {
|
||||
PackageName string `bson:"package_name" json:"package_name"`
|
||||
Installed string `bson:"installed_version" json:"installed_version"`
|
||||
// FixedIn empty means no vendor fix has been published. That is a real and
|
||||
// common state and must never be conflated with "not vulnerable" — it is
|
||||
// common state and must never be conflated with "not vulnerable" - it is
|
||||
// the finding most in need of acceptance, since there is nothing to patch.
|
||||
FixedIn string `bson:"fixed_in,omitempty" json:"fixed_in,omitempty"`
|
||||
Severity string `bson:"severity" json:"severity"`
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// dispatchSMTP delivers a state change over one channel's own SMTP settings.
|
||||
//
|
||||
// The transport, the envelope and the look of the message all live in
|
||||
// shared/mail, which admin and sitesvc use too — a Vantage alert and a Vantage
|
||||
// shared/mail, which admin and sitesvc use too - a Vantage alert and a Vantage
|
||||
// licence email should not look like they came from different products. This
|
||||
// function only turns a channel document into a Sender.
|
||||
func dispatchSMTP(ch models.NotificationChannel, ev Event) error {
|
||||
|
||||
@@ -16,7 +16,7 @@ const TypeVuln = "vulnerability"
|
||||
//
|
||||
// One per rule per scan, never one per finding: a database refresh can open
|
||||
// several hundred at once, and a message each would rate-limit the webhook or
|
||||
// get the channel muted — either way the alerts stop being read.
|
||||
// get the channel muted - either way the alerts stop being read.
|
||||
type VulnDigest struct {
|
||||
InstanceName string
|
||||
RuleName string
|
||||
@@ -74,7 +74,7 @@ func vulnLines(d VulnDigest) string {
|
||||
if r.FixedIn != "" {
|
||||
fix = "fixed in " + r.FixedIn
|
||||
}
|
||||
s += fmt.Sprintf("\n• %s (%s) — %s on %s, %s", r.CVEID, r.Severity, r.PackageName, r.ServerName, fix)
|
||||
s += fmt.Sprintf("\n• %s (%s) - %s on %s, %s", r.CVEID, r.Severity, r.PackageName, r.ServerName, fix)
|
||||
}
|
||||
if d.More > 0 {
|
||||
s += fmt.Sprintf("\n…and %d more.", d.More)
|
||||
|
||||
@@ -34,4 +34,4 @@ func NewID() (string, error) {
|
||||
//
|
||||
// The pending record lives in Redis instead (bus.SetPendingProxy /
|
||||
// ClaimPendingProxy), and the listener is bound by whichever pod the stream
|
||||
// actually reaches — see services.ClaimProxyStream.
|
||||
// actually reaches - see services.ClaimProxyStream.
|
||||
@@ -90,7 +90,7 @@ func (s *Session) Reason() string {
|
||||
// a no-op once a deliberate teardown (Close) has begun: a local Close closing
|
||||
// the conn out from under the relay goroutines produces exactly the kind of
|
||||
// error (net.ErrClosed, a broken pipe on write, ...) that looks like a remote
|
||||
// failure but is not one, and must not overwrite — or race to set — the real
|
||||
// failure but is not one, and must not overwrite - or race to set - the real
|
||||
// reason, or invent one where a clean local close has none.
|
||||
func (s *Session) setReason(r string) {
|
||||
s.mu.Lock()
|
||||
@@ -103,7 +103,7 @@ func (s *Session) setReason(r string) {
|
||||
// Close tears the session down once. A non-empty reason is recorded only if no
|
||||
// reason has been recorded already, and only before teardown begins. It closes
|
||||
// both the listener and, if a connection has already been accepted, that
|
||||
// connection too — an unconditional kill for the whole relay chain regardless
|
||||
// connection too - an unconditional kill for the whole relay chain regardless
|
||||
// of which stage it is in.
|
||||
func (s *Session) Close(reason string) {
|
||||
if reason != "" {
|
||||
|
||||
@@ -35,15 +35,15 @@ func LogEvent(instanceID, eventType, actor, serverID, keyID, details string) {
|
||||
// AuditFilter narrows a page of the audit log.
|
||||
//
|
||||
// Filtering is done here rather than in the browser because the audit log is
|
||||
// the one collection deliberately kept for months — audit_retention_days is a
|
||||
// licensed entitlement — and it is read to answer questions about the past
|
||||
// the one collection deliberately kept for months - audit_retention_days is a
|
||||
// licensed entitlement - and it is read to answer questions about the past
|
||||
// ("who removed that key in March"). A browser filtering the most recent 200
|
||||
// rows would answer "no results" for an event that exists, which is worse than
|
||||
// having no search at all.
|
||||
type AuditFilter struct {
|
||||
// Search matches actor, details or event type, case-insensitively.
|
||||
Search string
|
||||
// Category matches the segment before the first dot in an event type —
|
||||
// Category matches the segment before the first dot in an event type -
|
||||
// "workflow", "key", "server". Event types are named consistently enough
|
||||
// that the prefix is a real grouping rather than a guess.
|
||||
Category string
|
||||
|
||||
@@ -44,7 +44,7 @@ func StartAuditSweeper(ctx context.Context) {
|
||||
// process restarts.
|
||||
//
|
||||
// It skips an instance whose licence is not valid. A lapsed instance must not
|
||||
// have its history trimmed on the expired term's allowance — expiry degrades to
|
||||
// have its history trimmed on the expired term's allowance - expiry degrades to
|
||||
// read-only, and deleting more of somebody's audit trail is not read-only.
|
||||
//
|
||||
// It skips Unlimited and any non-positive value. A licence that decodes as zero
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
// ErrLockout is returned when a change would leave an instance with neither
|
||||
// local password login nor an enabled provider — nobody could sign in, and no
|
||||
// local password login nor an enabled provider - nobody could sign in, and no
|
||||
// endpoint exists to undo it without database access.
|
||||
var ErrLockout = errors.New("that would leave nobody able to sign in")
|
||||
|
||||
@@ -36,8 +36,8 @@ func authProviderCtx() (context.Context, context.CancelFunc) {
|
||||
return context.WithTimeout(context.Background(), 5*time.Second)
|
||||
}
|
||||
|
||||
// CheckLockout is pure so the two endpoints that can reach this condition —
|
||||
// saving settings and changing a provider — share one answer.
|
||||
// CheckLockout is pure so the two endpoints that can reach this condition -
|
||||
// saving settings and changing a provider - share one answer.
|
||||
func CheckLockout(localEnabled bool, enabledProviders int) error {
|
||||
if localEnabled || enabledProviders > 0 {
|
||||
return nil
|
||||
@@ -244,8 +244,8 @@ func IsLocalLoginEnabled(instanceID string) bool {
|
||||
|
||||
// LocalLoginPermitted answers whether password sign-in must be accepted for
|
||||
// this instance, which is not the same question as whether an administrator
|
||||
// turned it on. An instance whose only providers have become unusable — a
|
||||
// lapsed licence, or every provider disabled — has to keep its password form,
|
||||
// turned it on. An instance whose only providers have become unusable - a
|
||||
// lapsed licence, or every provider disabled - has to keep its password form,
|
||||
// or nobody can sign in and there is no endpoint left to fix it with.
|
||||
func LocalLoginPermitted(instanceID string) bool {
|
||||
if IsLocalLoginEnabled(instanceID) {
|
||||
|
||||
@@ -110,7 +110,7 @@ func UpdateChannel(instanceID, channelID string, upd bson.M) error {
|
||||
// save arrives carrying the sentinel in place of the password. Writing it
|
||||
// through would replace the credential with eight bullet characters and break
|
||||
// delivery on the next alert. A value that is not the sentinel is written
|
||||
// verbatim — including the empty string, which is how a credential is cleared.
|
||||
// verbatim - including the empty string, which is how a credential is cleared.
|
||||
func mergeChannelSecrets(instanceID, channelID string, upd bson.M, cfg map[string]string) (map[string]string, error) {
|
||||
stored, err := GetChannel(instanceID, channelID)
|
||||
if err != nil {
|
||||
|
||||
@@ -30,7 +30,7 @@ var ErrAgentOffline = errors.New("agent is not connected")
|
||||
//
|
||||
// That third line is the one that is easy to get wrong. A ProxyStream is a
|
||||
// separate HTTP/2 request, and an L7 proxy (Traefik, which the chart's gRPC
|
||||
// ingress uses) balances requests rather than connections — so it does not
|
||||
// ingress uses) balances requests rather than connections - so it does not
|
||||
// follow the command stream. Binding the relay listener on the command
|
||||
// stream's pod therefore fails roughly (n-1)/n of the time with "proxy session
|
||||
// not found": the stream arrives at a pod whose registry is empty.
|
||||
@@ -48,7 +48,7 @@ var ErrAgentOffline = errors.New("agent is not connected")
|
||||
//
|
||||
// Teardown needs no message of its own. When the browser goes away guac closes
|
||||
// its connection to the relay, the relay sees the read end, and the session
|
||||
// closes itself — the same path a single-process deployment always took. Only
|
||||
// closes itself - the same path a single-process deployment always took. Only
|
||||
// the *reason* has to cross back, because the pod that writes the audit event
|
||||
// is not the pod that observed the failure.
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
//
|
||||
// An agent's CommandStream terminates on exactly one process, and with several
|
||||
// replicas that is almost never the process handling the REST request that
|
||||
// wants to talk to it. Publishing unconditionally — rather than checking for a
|
||||
// local stream first and falling back — means one code path, exercised on every
|
||||
// wants to talk to it. Publishing unconditionally - rather than checking for a
|
||||
// local stream first and falling back - means one code path, exercised on every
|
||||
// deployment including the single-replica ones, instead of a rare cross-pod
|
||||
// path that only fails in production.
|
||||
const (
|
||||
@@ -48,7 +48,7 @@ type CommandEnvelope struct {
|
||||
|
||||
// Node names the pod this envelope is for: the presence holder at the time
|
||||
// it was published. The command channel is a fan-out, so during a reconnect
|
||||
// two pods can be subscribed for one agent — the pod with the live stream,
|
||||
// two pods can be subscribed for one agent - the pod with the live stream,
|
||||
// and a pod whose stream is half-open and has not yet noticed. Both would
|
||||
// receive the envelope, and the first to ack wins the request. If that is
|
||||
// the stale one, the command is queued onto a dead stream and acked OK: the
|
||||
@@ -161,7 +161,7 @@ func (d *commandDispatcher) Serve(ctx context.Context, serverID string) (<-chan
|
||||
//
|
||||
// A Redis failure returns true. It is tempting to read an error as loss and
|
||||
// give up, but nothing is known in that moment about who holds the claim, and
|
||||
// the stream this pod is serving is demonstrably alive — the caller is either a
|
||||
// the stream this pod is serving is demonstrably alive - the caller is either a
|
||||
// ticker on that stream or a beat that just succeeded on it. Standing down on a
|
||||
// blip is precisely how an agent ends up connected, beating, and unreachable
|
||||
// until it happens to reconnect.
|
||||
@@ -174,7 +174,7 @@ func renewPresence(ctx context.Context, serverID string) bool {
|
||||
log.Printf("dispatch: presence for %s is held elsewhere, standing down", serverID)
|
||||
return false
|
||||
case bus.RenewedClaim:
|
||||
// Nobody held the key — Redis restarted, failed over, evicted it, or was
|
||||
// Nobody held the key - Redis restarted, failed over, evicted it, or was
|
||||
// unreachable for longer than the TTL. Worth a line: it is the only
|
||||
// evidence that presence was lost and recovered rather than never lost.
|
||||
log.Printf("dispatch: reclaimed presence for %s", serverID)
|
||||
|
||||
@@ -30,7 +30,7 @@ func findingKey(cveID, pkg string) string { return cveID + "\x00" + pkg }
|
||||
// DiffFindings computes the state changes for one server's scan.
|
||||
//
|
||||
// Pure by design: no database, no clock of its own. The ordering below is
|
||||
// load-bearing — see the comment above the second loop.
|
||||
// load-bearing - see the comment above the second loop.
|
||||
func DiffFindings(existing []models.VulnFinding, results []vulndb.Result, now time.Time) FindingDiff {
|
||||
var d FindingDiff
|
||||
|
||||
@@ -104,7 +104,7 @@ func DiffFindings(existing []models.VulnFinding, results []vulndb.Result, now ti
|
||||
}
|
||||
|
||||
// ErrFindingNotFound is returned for a finding that does not exist in this
|
||||
// instance. Callers turn it into a 404 — never a 403, which would confirm the
|
||||
// instance. Callers turn it into a 404 - never a 403, which would confirm the
|
||||
// finding exists in someone else's instance.
|
||||
var ErrFindingNotFound = errors.New("finding not found")
|
||||
|
||||
@@ -116,7 +116,7 @@ type FindingFilter struct {
|
||||
ServerID string
|
||||
Tags map[string]string
|
||||
// HasFix nil is no filter. true is "a vendor fix exists, this is
|
||||
// patchable"; false is the unfixable set — remove the package, disable the
|
||||
// patchable"; false is the unfixable set - remove the package, disable the
|
||||
// service, or accept it, but do not wait for an update.
|
||||
HasFix *bool
|
||||
// TokenScope is the acting credential's tag restriction, nil meaning
|
||||
@@ -172,7 +172,7 @@ func ListInstanceFindings(instanceID string, f FindingFilter) ([]models.VulnFind
|
||||
}
|
||||
|
||||
// The token restriction is applied the same way the Tags selector above
|
||||
// is — by narrowing server_id — rather than by a post-pass, so the two
|
||||
// is - by narrowing server_id - rather than by a post-pass, so the two
|
||||
// cannot disagree and the query keeps one shape. IntersectSelectors is
|
||||
// not used here because Tags has already been resolved to IDs by this
|
||||
// point; intersecting the ID sets is the same operation one level down.
|
||||
@@ -370,7 +370,7 @@ func ListFindings(ctx context.Context, instanceID, serverID string) ([]models.Vu
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ApplyFindingDiff writes a diff. Thin on purpose — the logic worth reading
|
||||
// ApplyFindingDiff writes a diff. Thin on purpose - the logic worth reading
|
||||
// twice is all in DiffFindings.
|
||||
func ApplyFindingDiff(ctx context.Context, instanceID, serverID string, d FindingDiff, now time.Time) error {
|
||||
col := db.Col("vuln_findings")
|
||||
|
||||
@@ -122,7 +122,7 @@ type KeyWithCount struct {
|
||||
// count, narrowed by tokenScope: AssignedCount only counts assignments on
|
||||
// servers ServerInTokenScope admits. Without this, a restricted token reading
|
||||
// the key list would see a nonzero count for a key it cannot see a single
|
||||
// assignment of in its own scope — the same hostname-existence leak
|
||||
// assignment of in its own scope - the same hostname-existence leak
|
||||
// getKey's scope filter closes on the detail route, reachable here through a
|
||||
// count instead of a server object.
|
||||
//
|
||||
@@ -148,7 +148,7 @@ func ListKeys(instanceID string, tokenScope map[string]string) ([]KeyWithCount,
|
||||
|
||||
// Resolve the visible fleet once, outside the per-key loop, so a
|
||||
// restricted token's count costs one extra query total rather than one
|
||||
// per key — the same reasoning ResolveTargetsScoped already applies to
|
||||
// per key - the same reasoning ResolveTargetsScoped already applies to
|
||||
// target resolution.
|
||||
scoped := len(tokenScope) > 0
|
||||
var visibleIDs []string
|
||||
|
||||
@@ -22,7 +22,7 @@ type LicenseState struct {
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
Limits license.Limits `json:"limits"`
|
||||
Features map[string]bool `json:"features"`
|
||||
// Source is "stored", "env" or "none" — useful when a self-hosted operator
|
||||
// Source is "stored", "env" or "none" - useful when a self-hosted operator
|
||||
// asks why the licence they pasted is not the one in effect.
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func limitCtx() (context.Context, context.CancelFunc) {
|
||||
// CheckServerLimit refuses a new server when the instance is at its cap.
|
||||
//
|
||||
// Counts live rows only. An instance already over its cap keeps every server it
|
||||
// has — nothing is truncated — it simply cannot add another.
|
||||
// has - nothing is truncated - it simply cannot add another.
|
||||
func CheckServerLimit(instanceID string) error {
|
||||
st := GetLicenseState(instanceID)
|
||||
ctx, cancel := limitCtx()
|
||||
@@ -92,7 +92,7 @@ func CheckChannelLimit(instanceID string) error {
|
||||
// CheckMonitorLimit refuses a new monitor when the instance is at its cap.
|
||||
//
|
||||
// Counts live rows only, like every other check here. An instance already over
|
||||
// its cap keeps every monitor it has and they keep executing — the licence
|
||||
// its cap keeps every monitor it has and they keep executing - the licence
|
||||
// expiry story is that monitoring never stops, so truncating here would
|
||||
// contradict it.
|
||||
func CheckMonitorLimit(instanceID string) error {
|
||||
|
||||
@@ -27,7 +27,7 @@ type legacyInstanceOIDC struct {
|
||||
// migration that needs KEY_ENCRYPTION_KEY fails on an instance that has none
|
||||
// and strands the SSO configuration it was supposed to preserve.
|
||||
//
|
||||
// instance_oidc is left in place and no longer read. Nothing deletes it — a
|
||||
// instance_oidc is left in place and no longer read. Nothing deletes it - a
|
||||
// migration that drops the only copy of a client secret has no undo.
|
||||
func MigrateAuthProviders() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
//
|
||||
// Migration 0004 renames org_id to instance_id in each. A collection missing
|
||||
// from this list keeps the old field name and becomes invisible to every scoped
|
||||
// query — so this list is load-bearing, not documentation.
|
||||
// query - so this list is load-bearing, not documentation.
|
||||
//
|
||||
// AssertNoScopedCollectionMissed checks at boot that nothing outside this list
|
||||
// holds an org_id.
|
||||
@@ -63,7 +63,7 @@ var collectionRenames = []struct{ from, to string }{
|
||||
// bad deploy is recovered by running the inverse rename (cmd/rename-rollback)
|
||||
// rather than by restoring a backup.
|
||||
//
|
||||
// The steps are not atomic across collections — multi-document transactions
|
||||
// The steps are not atomic across collections - multi-document transactions
|
||||
// would require a replica set, which self-hosted installs do not guarantee.
|
||||
// Instead every step is safely repeatable: a collection rename is skipped when
|
||||
// the source is already gone, and $rename matches nothing on a document that
|
||||
|
||||
@@ -55,8 +55,8 @@ func SpecFor(m *models.Monitor) checker.Spec {
|
||||
|
||||
// RedactMonitorRunner replaces m.Runner with models.RunnerRestricted when it
|
||||
// names a server outside the caller's scope, so GET /monitors and GET
|
||||
// /monitors/:id can keep listing the monitor itself — name, type, state,
|
||||
// whether it exists at all — as the first-class object it is, without
|
||||
// /monitors/:id can keep listing the monitor itself - name, type, state,
|
||||
// whether it exists at all - as the first-class object it is, without
|
||||
// disclosing which out-of-scope server it happens to run on. Omitting the
|
||||
// monitor entirely was considered and rejected: a restricted operator has a
|
||||
// legitimate reason to see that a monitor exists and is up or down even when
|
||||
@@ -293,7 +293,7 @@ const MaxMonitorSamples = 6000
|
||||
|
||||
// MonitorSamples returns individual check results since a point in time,
|
||||
// oldest first. Samples older than MonitorSampleTTL have expired, so an early
|
||||
// `since` silently returns a shorter window rather than an error — the caller
|
||||
// `since` silently returns a shorter window rather than an error - the caller
|
||||
// draws the gap.
|
||||
func MonitorSamples(instanceID, monitorID string, since time.Time) ([]models.MonitorSample, error) {
|
||||
ctx, cancel := monCtx()
|
||||
|
||||
@@ -21,7 +21,7 @@ const MonitorSampleTTL = 48 * time.Hour
|
||||
|
||||
// EnsureMonitorSampleIndexes declares the sample range index and its TTL.
|
||||
//
|
||||
// Warn rather than fatal, like the other history indexes — but note the TTL is
|
||||
// Warn rather than fatal, like the other history indexes - but note the TTL is
|
||||
// not an optimisation: without it nothing ever removes a sample, and the
|
||||
// collection grows at the fleet's total check rate forever. A boot that logs
|
||||
// this warning needs following up.
|
||||
|
||||
@@ -87,7 +87,7 @@ type PackageHit struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
// SearchPackages answers "which servers run package X" across the fleet — the
|
||||
// SearchPackages answers "which servers run package X" across the fleet - the
|
||||
// question people actually ask during an incident.
|
||||
//
|
||||
// The Mongo filter narrows to documents containing the name; the second pass is
|
||||
@@ -96,8 +96,8 @@ type PackageHit struct {
|
||||
//
|
||||
// tokenScope is the acting credential's tag restriction, nil meaning
|
||||
// unrestricted; a hit on a server outside it is dropped before it is returned.
|
||||
// The filtering is done with VisibleServerIDs — one membership set resolved
|
||||
// once — rather than by resolving each hit's server individually the way
|
||||
// The filtering is done with VisibleServerIDs - one membership set resolved
|
||||
// once - rather than by resolving each hit's server individually the way
|
||||
// search_fleet does, because a package search can return one hit per host in
|
||||
// the fleet and the query shape must not depend on how many matched. The Mongo
|
||||
// query itself is unchanged: server_packages carries no tags to filter on, so
|
||||
|
||||
@@ -62,7 +62,7 @@ func reapAfter() time.Duration {
|
||||
//
|
||||
// Unexported and unguarded: it trusts its caller completely and performs an
|
||||
// irreversible delete on whatever instance ID it is handed. The tier and expiry
|
||||
// gate — Free tier, an expiry that exists, an expiry past the window — lives in
|
||||
// gate - Free tier, an expiry that exists, an expiry past the window - lives in
|
||||
// ReapFreeInstances, which is the only caller. Do not export this.
|
||||
//
|
||||
// Idempotent: re-running over a half-deleted instance completes it. The instance
|
||||
@@ -93,8 +93,8 @@ func purgeInstance(ctx context.Context, instanceID string) (map[string]int64, er
|
||||
// ago than the configured window.
|
||||
//
|
||||
// Eligibility requires ALL of:
|
||||
// - license_tier == "free" — a paid instance is never eligible
|
||||
// - license_expiry present — an instance that was never licensed, or whose
|
||||
// - license_tier == "free" - a paid instance is never eligible
|
||||
// - license_expiry present - an instance that was never licensed, or whose
|
||||
// issuance failed, has no expiry and is never eligible whatever its age
|
||||
// - license_expiry older than now minus the window
|
||||
//
|
||||
@@ -136,7 +136,7 @@ func ReapFreeInstances(ctx context.Context) (checked, purged int, err error) {
|
||||
// audit entry written here would delete itself moments later. It is
|
||||
// written anyway, because an operator reading audit during the window
|
||||
// should see it coming.
|
||||
log.Printf("REAPING instance %s (%s, slug=%s) — Free licence expired %s, past the %s window",
|
||||
log.Printf("REAPING instance %s (%s, slug=%s) - Free licence expired %s, past the %s window",
|
||||
d.InstanceID, d.Name, d.Slug, d.Expiry.Format(time.RFC3339), window)
|
||||
LogEvent(d.InstanceID, "instance.reaped", "system", "", "",
|
||||
fmt.Sprintf("free licence expired %s, window %s", d.Expiry.Format(time.RFC3339), window))
|
||||
@@ -157,7 +157,7 @@ func ReapFreeInstances(ctx context.Context) (checked, purged int, err error) {
|
||||
//
|
||||
// The pass at boot follows inject.StartReconciler's precedent and earns its keep
|
||||
// the same way: it makes a restart a supported way to force a sweep, which is
|
||||
// the only way this code can be exercised on demand — the ticker is hourly and
|
||||
// the only way this code can be exercised on demand - the ticker is hourly and
|
||||
// deletion is measured in days.
|
||||
func StartReaper(ctx context.Context) {
|
||||
window := reapAfter()
|
||||
@@ -165,7 +165,7 @@ func StartReaper(ctx context.Context) {
|
||||
log.Printf("reaper: DISABLED (FREE_INSTANCE_REAP_AFTER is unset or zero)")
|
||||
return
|
||||
}
|
||||
log.Printf("reaper: ENABLED — Free instances are deleted %s after their licence expires", window)
|
||||
log.Printf("reaper: ENABLED - Free instances are deleted %s after their licence expires", window)
|
||||
|
||||
go func() {
|
||||
reapOnce(ctx)
|
||||
|
||||
@@ -76,7 +76,7 @@ func GetServer(instanceID, serverID string) (*models.Server, error) {
|
||||
// a restricted token must not be able to enumerate the fleet it cannot see by
|
||||
// noticing which IDs answer differently.
|
||||
//
|
||||
// mongo.ErrNoDocuments is GetServer's own not-found identifier — reused here
|
||||
// mongo.ErrNoDocuments is GetServer's own not-found identifier - reused here
|
||||
// rather than introducing a second one, so a caller checking for one keeps
|
||||
// working against a server that exists but is out of the token's scope.
|
||||
func GetServerScoped(instanceID, serverID string, tokenScope map[string]string) (*models.Server, error) {
|
||||
@@ -360,7 +360,7 @@ func markOfflineForFilter(scope bson.M, instanceID string) error {
|
||||
}
|
||||
|
||||
// notifyServerOffline delivers an agent-offline alert over the instance's
|
||||
// chosen notification channels — the same destinations monitors dispatch to,
|
||||
// chosen notification channels - the same destinations monitors dispatch to,
|
||||
// so a webhook or SMTP destination is configured and tested in exactly one
|
||||
// place. No channels selected means the alert is audited but not sent.
|
||||
func notifyServerOffline(instanceID string, channelIDs []string, s models.Server) {
|
||||
@@ -393,7 +393,7 @@ func notifyServerOffline(instanceID string, channelIDs []string, s models.Server
|
||||
}
|
||||
|
||||
// ListServersFiltered is ListServers with an optional tag selector. An empty
|
||||
// selector returns the whole fleet — unlike MatchesTags, where empty means
|
||||
// selector returns the whole fleet - unlike MatchesTags, where empty means
|
||||
// "nothing", because here the caller is a list view whose default is
|
||||
// "everything", not a run about to touch machines.
|
||||
func ListServersFiltered(instanceID string, sel map[string]string) ([]models.Server, error) {
|
||||
|
||||
@@ -74,7 +74,7 @@ func validateIncident(inc *models.StatusIncident) error {
|
||||
// fleet's monitors: publishing "api-gateway is degraded" on a page that never
|
||||
// listed api-gateway names a machine to the public that the page deliberately
|
||||
// does not, which is the same leak assembleSnapshot's redaction boundary exists
|
||||
// to prevent — reached from the authoring side instead of the read side.
|
||||
// to prevent - reached from the authoring side instead of the read side.
|
||||
//
|
||||
// It is a separate pass rather than part of validateIncident because it reads
|
||||
// the database, and validateIncident is a pure function of the document. The
|
||||
@@ -289,7 +289,7 @@ func AppendStatusIncidentUpdate(instanceID, incidentID, status, body, author str
|
||||
set["resolved_at"] = upd.At
|
||||
} else {
|
||||
// Reopening via an appended update must clear a previously-set
|
||||
// resolved_at the same way UpdateStatusIncident does — otherwise a
|
||||
// resolved_at the same way UpdateStatusIncident does - otherwise a
|
||||
// resolved incident reopened to "monitoring" keeps a stale resolved_at
|
||||
// and silently drops off ListStatusIncidentsForPage once started_at
|
||||
// ages past the since cutoff, because none of its $or clauses match.
|
||||
|
||||
@@ -47,7 +47,7 @@ func spCtx() (context.Context, context.CancelFunc) {
|
||||
//
|
||||
// All three are attempted and the failures joined, rather than returning on
|
||||
// the first. The three are independent, and two of them are uniqueness
|
||||
// constraints — bailing out on the status_pages index meant a transient
|
||||
// constraints - bailing out on the status_pages index meant a transient
|
||||
// failure there silently left status_incidents with no unique
|
||||
// (instance_id, incident_id) index at all.
|
||||
func EnsureStatusPageIndexes() error {
|
||||
@@ -86,7 +86,7 @@ var (
|
||||
ErrPageIDTaken = errors.New("that page id is already in use")
|
||||
|
||||
// ErrPageInvalid is the sentinel for validation failures on a page or
|
||||
// incident body — anything the caller can fix by sending a different
|
||||
// incident body - anything the caller can fix by sending a different
|
||||
// request. statusPageError maps it to 400; wrap it rather than returning a
|
||||
// bare error, or a bad request answers 500.
|
||||
ErrPageInvalid = errors.New("status page request invalid")
|
||||
|
||||
@@ -58,7 +58,7 @@ type PublicIncidentUpdate struct {
|
||||
}
|
||||
|
||||
// PublicIncident covers both authored incidents and derived monitor outages.
|
||||
// A derived one carries no updates and no impact — and never a cause, which is
|
||||
// A derived one carries no updates and no impact - and never a cause, which is
|
||||
// where internal hostnames live.
|
||||
type PublicIncident struct {
|
||||
ID string `json:"id"`
|
||||
@@ -138,7 +138,7 @@ func assembleSnapshot(in snapshotInput) StatusSnapshot {
|
||||
names[entry.MonitorID] = name
|
||||
|
||||
// Uptime is computed from the days as reported by rollups, before
|
||||
// any maintenance repaint — a no_data day must never be counted as
|
||||
// any maintenance repaint - a no_data day must never be counted as
|
||||
// zero uptime just because it is later redrawn as "maintenance".
|
||||
days := buildDays(in.Rollups[entry.MonitorID], in.Now)
|
||||
comp := PublicComponent{
|
||||
@@ -294,7 +294,7 @@ func publicFromAuthored(inc models.StatusIncident, names map[string]string) Publ
|
||||
// already read the true no_data/up/down state of each day. Folding the
|
||||
// repaint in here would let a today cell with no rollups yet flip from
|
||||
// no_data to maintenance before its uptime contribution was decided, and
|
||||
// uptimeFromDays skips no_data days by their State — so that day would stop
|
||||
// uptimeFromDays skips no_data days by their State - so that day would stop
|
||||
// being skipped and start counting as a zero.
|
||||
func buildDays(rollups []models.Rollup, now time.Time) []PublicDay {
|
||||
type bucket struct{ checks, up int }
|
||||
@@ -335,7 +335,7 @@ func buildDays(rollups []models.Rollup, now time.Time) []PublicDay {
|
||||
// computation, not before: repainting first would turn a today cell with no
|
||||
// rollups yet from no_data (skipped) into maintenance (a 0% day counted in
|
||||
// the average), and repainting a day that DOES have rollups must still leave
|
||||
// that day's real up/down contribution in the average — maintenance changes
|
||||
// that day's real up/down contribution in the average - maintenance changes
|
||||
// how a day is drawn, never what the numbers say.
|
||||
func applyMaintenanceRepaint(days []PublicDay, inMaintenance bool) []PublicDay {
|
||||
if inMaintenance && len(days) > 0 {
|
||||
|
||||
@@ -165,7 +165,7 @@ func TestAssembleSnapshotMaintenanceDoesNotChangeUptime(t *testing.T) {
|
||||
|
||||
// TestAssembleSnapshotMaintenanceRepaintDoesNotCountNoDataAsZero guards
|
||||
// against the maintenance repaint corrupting Uptime90d for a component whose
|
||||
// today rollup has not landed yet — an in-progress maintenance window on a
|
||||
// today rollup has not landed yet - an in-progress maintenance window on a
|
||||
// young component, or one that simply started before today's hourly rollup
|
||||
// was written. Repainting today's no_data cell to "maintenance" must never
|
||||
// make uptimeFromDays stop skipping it: doing so would turn a component with
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
// process: step output arrives on whichever pod holds the agent's stream, the
|
||||
// run's markers are written by whichever pod started the run, and the browser
|
||||
// asks for the log through whichever pod the load balancer picked. Three pods,
|
||||
// one file, one local disk — two of them see an empty log.
|
||||
// one file, one local disk - two of them see an empty log.
|
||||
//
|
||||
// Mongo makes every pod an equal reader and writer, which is the property that
|
||||
// matters. It costs writes on the hot path, so the writer batches (see
|
||||
@@ -144,7 +144,7 @@ type stepLogRegistry struct {
|
||||
|
||||
// The registry stays process-local, and correctly so: a step's output arrives
|
||||
// on the pod holding that agent's stream, and that is the same pod the
|
||||
// dispatch envelope asked to open the writer. Nothing here crosses pods —
|
||||
// dispatch envelope asked to open the writer. Nothing here crosses pods -
|
||||
// only the lines it produces do, by virtue of landing in Mongo.
|
||||
var StepLogs = &stepLogRegistry{writers: make(map[string]*stepLogWriter)}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ type stepResultRegistry struct{}
|
||||
var StepResults = &stepResultRegistry{}
|
||||
|
||||
// Await subscribes to a command's result channel. The returned cancel function
|
||||
// must be called once the caller is done, whether a result arrived or not —
|
||||
// must be called once the caller is done, whether a result arrived or not -
|
||||
// it is what releases the Redis subscription.
|
||||
func (r *stepResultRegistry) Await(commandID string) (<-chan *pb.StepResult, func()) {
|
||||
out := make(chan *pb.StepResult, 1)
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
// ErrInvalidTag is returned for any tag the rules below reject. Handlers map
|
||||
// it to 400 — a malformed tag is the caller's mistake, not a server fault.
|
||||
// it to 400 - a malformed tag is the caller's mistake, not a server fault.
|
||||
var ErrInvalidTag = errors.New("invalid tag")
|
||||
|
||||
const (
|
||||
@@ -43,8 +43,8 @@ func validTagRunes(s string) bool {
|
||||
}
|
||||
|
||||
// ValidateTags enforces the shape of a whole tag map. It lives in the service
|
||||
// layer rather than a handler so that every write path — the tags endpoint,
|
||||
// server create, anything added later — agrees on what a valid tag is.
|
||||
// layer rather than a handler so that every write path - the tags endpoint,
|
||||
// server create, anything added later - agrees on what a valid tag is.
|
||||
func ValidateTags(tags map[string]string) error {
|
||||
if len(tags) > maxTagsPerHost {
|
||||
return fmt.Errorf("%w: at most %d tags per server", ErrInvalidTag, maxTagsPerHost)
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
// report success over zero servers.
|
||||
var ErrNoTargets = errors.New("workflow has no target servers")
|
||||
|
||||
// MatchesTags reports whether srv carries every pair in sel — AND across keys.
|
||||
// MatchesTags reports whether srv carries every pair in sel - AND across keys.
|
||||
// An empty selector matches nothing. That is deliberate: the alternative,
|
||||
// "matches everything", turns a cleared field in the workflow designer into a
|
||||
// fleet-wide run.
|
||||
|
||||
@@ -129,7 +129,7 @@ func CreateAPIToken(instanceID, userID, name, role string, scopes []string, tagS
|
||||
InstanceID: instanceID,
|
||||
UserID: userID,
|
||||
Name: name,
|
||||
// Hint is "vt_" plus 5 hex characters of the secret (20 bits) — enough
|
||||
// Hint is "vt_" plus 5 hex characters of the secret (20 bits) - enough
|
||||
// for a user to recognise their own token in a list, not enough to be
|
||||
// useful to anyone who only has the hint. Considered and accepted.
|
||||
Hint: plaintext[:8],
|
||||
@@ -197,7 +197,7 @@ var (
|
||||
)
|
||||
|
||||
// ShouldLogExpiredTokenUse reports whether an expired token's use is worth a
|
||||
// fresh audit row, throttled to once per token per minute — the same window
|
||||
// fresh audit row, throttled to once per token per minute - the same window
|
||||
// TouchAPIToken uses for last-used, kept here rather than in the auth package
|
||||
// because the storage concern (what counts as "recent") belongs beside the
|
||||
// token's other storage-backed state, not scattered into the request layer.
|
||||
|
||||
@@ -25,13 +25,13 @@ func ServerInTokenScope(srv models.Server, sel map[string]string) bool {
|
||||
|
||||
// VisibleServerIDs resolves the servers tokenScope admits into a membership
|
||||
// set, for a caller that needs to test many IDs against the caller's scope in
|
||||
// one pass — redacting a monitor's runner, filtering a workflow's target list
|
||||
// — rather than resolving one server at a time the way GetServerScoped does.
|
||||
// one pass - redacting a monitor's runner, filtering a workflow's target list
|
||||
// - rather than resolving one server at a time the way GetServerScoped does.
|
||||
//
|
||||
// restricted is false for an empty tokenScope, matching ServerInTokenScope's
|
||||
// own rule that an empty selector is unrestricted rather than "sees nothing".
|
||||
// ids is then nil, and callers must treat (nil, false) as "everything
|
||||
// visible", never as "nothing visible" — the zero value of a map read is
|
||||
// visible", never as "nothing visible" - the zero value of a map read is
|
||||
// false, which would silently invert the rule for every unrestricted caller
|
||||
// if this contract were not honoured.
|
||||
func VisibleServerIDs(instanceID string, tokenScope map[string]string) (ids map[string]bool, restricted bool, err error) {
|
||||
@@ -55,7 +55,7 @@ func VisibleServerIDs(instanceID string, tokenScope map[string]string) (ids map[
|
||||
// (ids, restricted) pair VisibleServerIDs returns. An unrestricted caller
|
||||
// (restricted false) gets ids back unchanged and hidden is always false.
|
||||
//
|
||||
// hidden reports only whether at least one id was dropped — never how many —
|
||||
// hidden reports only whether at least one id was dropped - never how many -
|
||||
// because the point of surfacing it at all is to let a caller say "some
|
||||
// targets are not visible to you" without the count itself becoming the leak
|
||||
// this exists to close. A workflow that targets both an in-scope and an
|
||||
|
||||
@@ -136,7 +136,7 @@ func TestScopedRunOfOutOfScopeWorkflowReachesNothing(t *testing.T) {
|
||||
t.Errorf("staging-scoped run resolved %v, want nothing", got)
|
||||
}
|
||||
// ResolveTargetsScoped turns that empty set into ErrNoTargets, which is
|
||||
// the same answer a workflow targeting no servers at all gives — so the
|
||||
// the same answer a workflow targeting no servers at all gives - so the
|
||||
// refusal does not tell the caller that production hosts exist.
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ var ErrLastOwner = errors.New("this is the organization's last owner promote ano
|
||||
// An hq-sourced row is projected from a Vantage HQ account: HQ owns its role,
|
||||
// its password and its existence. A role editable in two places is a role with
|
||||
// two answers, and the loser is whichever writer ran first. Refusing here
|
||||
// rather than merely hiding the control in web/ is the point — the API is the
|
||||
// rather than merely hiding the control in web/ is the point - the API is the
|
||||
// boundary, the UI is a courtesy.
|
||||
var ErrHQManaged = errors.New("this member is managed in Vantage HQ; change their role or remove them from the HQ portal")
|
||||
|
||||
@@ -77,7 +77,7 @@ func CreateUser(instanceID, email, password, role, authSource string) (*models.U
|
||||
//
|
||||
// There is deliberately no unscoped lookup by email. users is unique on
|
||||
// (instance_id, email), not on email alone, so an unscoped FindOne would return
|
||||
// an arbitrary one of several matching users — which on the login path means
|
||||
// an arbitrary one of several matching users - which on the login path means
|
||||
// signing someone into a tenant that is not theirs.
|
||||
func GetUserInInstanceByEmail(instanceID, email string) (*models.User, error) {
|
||||
email = strings.ToLower(strings.TrimSpace(email))
|
||||
|
||||
@@ -131,7 +131,7 @@ func validateVulnRule(instanceID string, r *models.VulnAlertRule) error {
|
||||
return validateChannelIDs(instanceID, r.ChannelIDs)
|
||||
}
|
||||
|
||||
// SendVulnDigest delivers one message per rule per tick — never one per
|
||||
// SendVulnDigest delivers one message per rule per tick - never one per
|
||||
// finding. See vulnsched for why the tick is the batch boundary.
|
||||
func SendVulnDigest(instanceID string, newly []models.VulnFinding) {
|
||||
rules, err := ListVulnRules(instanceID)
|
||||
|
||||
@@ -26,7 +26,7 @@ const stepDispatchGrace = 15 * time.Second
|
||||
// trigger would leave the dispatch reaching further than the readout.
|
||||
//
|
||||
// A run whose configured targets fall entirely outside the caller's scope
|
||||
// resolves to nothing and returns ErrNoTargets — the same answer a workflow
|
||||
// resolves to nothing and returns ErrNoTargets - the same answer a workflow
|
||||
// targeting no servers at all gives, so an out-of-scope host stays
|
||||
// indistinguishable from one that does not exist.
|
||||
func TriggerWorkflow(instanceID, workflowID, actor string, tokenScope map[string]string) (string, error) {
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestSaveTargetsRestrictedCallerCannotReachOutsideScope(t *testing.T) {
|
||||
func TestSaveTargetsUnrestrictedCallerUnaffected(t *testing.T) {
|
||||
// validateWorkflowTargetScope short-circuits before ever resolving the
|
||||
// fleet when tokenScope is nil, so an unrestricted caller keeps today's
|
||||
// behaviour exactly — including saving a tag selector matching nothing.
|
||||
// behaviour exactly - including saving a tag selector matching nothing.
|
||||
// listServersForScope is stubbed to fail the test if called at all, so
|
||||
// this proves the short-circuit, not just that the decision would allow
|
||||
// it.
|
||||
@@ -66,7 +66,7 @@ func TestSaveTargetsEqualOrNarrowerSelectorAllowed(t *testing.T) {
|
||||
}
|
||||
|
||||
// The time-of-write/time-of-fire gap: a restricted caller naming IDs or tags
|
||||
// that match no server at all today must be refused, not passed through —
|
||||
// that match no server at all today must be refused, not passed through -
|
||||
// otherwise the caller could save a selector for an environment that does
|
||||
// not exist yet, arm the schedule, and have it fire the moment a server picks
|
||||
// up the tag. This is distinct from "no targets at all" below.
|
||||
@@ -98,7 +98,7 @@ func TestSaveTargetsMatchingNothingIsRefusedForRestrictedCaller(t *testing.T) {
|
||||
// ...
|
||||
// FAIL .../internal/services 0.006s
|
||||
//
|
||||
// rather than a clean assertion failure, which is still a failure — the test
|
||||
// rather than a clean assertion failure, which is still a failure - the test
|
||||
// no longer passes silently once the enforcement is removed.
|
||||
func TestCreateAndUpdateWorkflowBindToTargetScopeCheck(t *testing.T) {
|
||||
restore := listServersForScope
|
||||
@@ -126,7 +126,7 @@ func TestCreateAndUpdateWorkflowBindToTargetScopeCheck(t *testing.T) {
|
||||
}
|
||||
|
||||
// A workflow with no targets at all (no IDs, no tags) must stay creatable
|
||||
// for a restricted caller — there is nothing to escalate through, and this
|
||||
// for a restricted caller - there is nothing to escalate through, and this
|
||||
// must not become collateral damage from the fix above.
|
||||
func TestSaveTargetsNoTargetsAtAllIsUnaffected(t *testing.T) {
|
||||
restore := listServersForScope
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
// ErrDefaultStep is returned when a caller tries to edit or delete a step that
|
||||
// came from the image's default library. Those rows are re-seeded from disk on
|
||||
// every boot, so an edit would be silently reverted and a delete would come
|
||||
// back — refusing is honest about who owns them.
|
||||
// back - refusing is honest about who owns them.
|
||||
var ErrDefaultStep = errors.New("this step ships with Vantage and cannot be edited or deleted; duplicate it to make your own copy")
|
||||
|
||||
func isDefaultStep(ctx context.Context, instanceID, stepID string) (bool, error) {
|
||||
@@ -304,7 +304,7 @@ func UpdateWorkflow(instanceID, id string, w models.Workflow, tokenScope map[str
|
||||
// The first is escalation: without it a token restricted to staging could save
|
||||
// a workflow targeting production and then reach those hosts through the
|
||||
// scheduler, which fires as the system with no restriction of its own. The
|
||||
// second is enumeration — "target server X not found" versus a successful save
|
||||
// second is enumeration - "target server X not found" versus a successful save
|
||||
// is a yes/no oracle over the whole fleet, and the design forbids a restricted
|
||||
// token learning which IDs exist outside its scope.
|
||||
//
|
||||
@@ -347,15 +347,15 @@ var listServersForScope = ListServers
|
||||
// A nil tokenScope is unrestricted and always passes: an unrestricted caller
|
||||
// may save any selector, including one matching nothing today, exactly as
|
||||
// before this fix. A workflow with no targets at all (empty IDs and empty
|
||||
// tags) is also left alone regardless of scope — there is nothing for it to
|
||||
// tags) is also left alone regardless of scope - there is nothing for it to
|
||||
// fire on, and refusing it would break the existing, unrelated ability to
|
||||
// save a workflow before wiring up its targets.
|
||||
//
|
||||
// What IS refused, for a restricted caller only, is a workflow that names IDs
|
||||
// or tags which resolve to no server at all. Without this, a token restricted
|
||||
// to env=staging could save target_tags {env: production} while no server yet
|
||||
// carries that pair — a not-yet-provisioned environment, a tag rollout in
|
||||
// progress, a guessed value — pass validation on an empty set, arm the
|
||||
// carries that pair - a not-yet-provisioned environment, a tag rollout in
|
||||
// progress, a guessed value - pass validation on an empty set, arm the
|
||||
// schedule, and have the scheduler execute on those hosts the moment someone
|
||||
// tags them. That is the same escalation as the out-of-scope case, just
|
||||
// deferred to whenever the fleet catches up to the selector, so it is
|
||||
|
||||
@@ -23,7 +23,7 @@ const workloadResultTimeout = 120 * time.Second
|
||||
|
||||
// MaxWorkloadLogLines mirrors the agent's own cap. It is declared again here
|
||||
// rather than imported: agent/ is a separate module with an internal/ tree, so
|
||||
// the two cannot share a constant. Change one, change the other — the same
|
||||
// the two cannot share a constant. Change one, change the other - the same
|
||||
// shape of hazard as the mirrored token blocks in the web apps.
|
||||
const MaxWorkloadLogLines = 500
|
||||
|
||||
@@ -33,7 +33,7 @@ const MaxWorkloadLogLines = 500
|
||||
const workloadProtectedMarker = "workload is protected"
|
||||
|
||||
// IsWorkloadProtected reports whether an agent refused because the target is
|
||||
// protected — the agent's own guard, which is the boundary. Nothing failed, so
|
||||
// protected - the agent's own guard, which is the boundary. Nothing failed, so
|
||||
// the API answers 409 rather than an error status.
|
||||
func IsWorkloadProtected(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), workloadProtectedMarker)
|
||||
@@ -91,14 +91,14 @@ type WorkloadHit struct {
|
||||
Workload models.Workload `json:"workload"`
|
||||
}
|
||||
|
||||
// SearchWorkloads answers "which servers run image X" — the reason the snapshot
|
||||
// SearchWorkloads answers "which servers run image X" - the reason the snapshot
|
||||
// is stored rather than fetched on demand and discarded.
|
||||
//
|
||||
// tokenScope is the acting credential's tag restriction, nil meaning
|
||||
// unrestricted. A WorkloadHit names a server ID, so an unfiltered fleet-wide
|
||||
// search enumerates hosts a restricted token must not see. server_workloads
|
||||
// carries no tags of its own, so the narrowing is a membership test against
|
||||
// VisibleServerIDs resolved once — the same shape SearchPackages uses.
|
||||
// VisibleServerIDs resolved once - the same shape SearchPackages uses.
|
||||
func SearchWorkloads(instanceID, image, stack, state string, tokenScope map[string]string) ([]WorkloadHit, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ type Advisory struct {
|
||||
// state, not an absence of data, and callers must treat it as vulnerable.
|
||||
FixedVersion string
|
||||
Severity string
|
||||
// Status is filled by trivy-db ONLY when FixedVersion is empty — when there
|
||||
// Status is filled by trivy-db ONLY when FixedVersion is empty - when there
|
||||
// is a fix, "fixed" is the obvious state and the field is left zero. It is
|
||||
// what separates "the vendor confirms this package is affected and has not
|
||||
// fixed it" from "nobody has looked yet".
|
||||
|
||||
@@ -8,7 +8,7 @@ type VulnSource interface {
|
||||
// MetaCache enriches match results with the CVE's own metadata.
|
||||
//
|
||||
// This is not an optimisation, it is where severity comes from. Debian, Ubuntu
|
||||
// and Alpine advisories carry no severity of their own — trivy-db leaves
|
||||
// and Alpine advisories carry no severity of their own - trivy-db leaves
|
||||
// Advisory.Severity zero for those buckets and keeps the rating in the
|
||||
// vulnerability bucket's VendorSeverity map instead. Taking the advisory's
|
||||
// value alone reported an entire fleet as "unknown".
|
||||
@@ -27,7 +27,7 @@ func NewMetaCache(src VulnSource) *MetaCache {
|
||||
// Enrich fills severity, title, score and references in place.
|
||||
//
|
||||
// The advisory's severity is kept when the vulnerability bucket has nothing
|
||||
// better to say — RHEL does publish it per advisory — so this can only raise
|
||||
// better to say - RHEL does publish it per advisory - so this can only raise
|
||||
// the quality of the answer, never lower it.
|
||||
func (m *MetaCache) Enrich(results []Result) {
|
||||
if m == nil {
|
||||
|
||||
@@ -16,7 +16,7 @@ type AdvisorySource interface {
|
||||
// Result is one vulnerable package on one server, before it becomes a finding.
|
||||
//
|
||||
// Severity, Title, CVSSScore and References are only as good as the advisory
|
||||
// until MetaCache.Enrich has run over them — for the Debian-family buckets the
|
||||
// until MetaCache.Enrich has run over them - for the Debian-family buckets the
|
||||
// advisory carries no severity at all, so an unenriched Result reads "unknown".
|
||||
type Result struct {
|
||||
CVEID string
|
||||
@@ -33,7 +33,7 @@ type Result struct {
|
||||
//
|
||||
// Vulnerable means: no fix has been published, or the installed version sorts
|
||||
// strictly before the fixed version under the distribution's own ordering.
|
||||
// Equal is NOT vulnerable — that is the backported-fix case, where a
|
||||
// Equal is NOT vulnerable - that is the backported-fix case, where a
|
||||
// distribution patches in place without changing the upstream version, and
|
||||
// treating it as vulnerable reports a patched fleet as exposed.
|
||||
func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPackage) ([]Result, error) {
|
||||
@@ -47,7 +47,7 @@ func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPacka
|
||||
var advisoryCount, skipped, unactionable, noFix int
|
||||
|
||||
// Advisories are keyed on the SOURCE package, and several hundred binary
|
||||
// packages on a host resolve to the same few hundred sources — linux-modules,
|
||||
// packages on a host resolve to the same few hundred sources - linux-modules,
|
||||
// linux-image and linux-headers all ask about "linux", whose advisory list is
|
||||
// thousands long. Without this the same bolt read is repeated once per binary
|
||||
// package, which is most of what made a single Ubuntu host take minutes.
|
||||
@@ -78,7 +78,7 @@ func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPacka
|
||||
|
||||
for _, a := range advs {
|
||||
// No published fix. Whether that is a finding depends entirely on the
|
||||
// status the vendor attached to it — see actionable().
|
||||
// status the vendor attached to it - see actionable().
|
||||
if a.FixedVersion == "" {
|
||||
if !actionable(a.Status) {
|
||||
unactionable++
|
||||
@@ -97,7 +97,7 @@ func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPacka
|
||||
if err != nil {
|
||||
// Skip this one advisory rather than failing the whole server:
|
||||
// one unparseable version must not blind us to every other CVE
|
||||
// on the host. Log it — a silent skip is a silent false
|
||||
// on the host. Log it - a silent skip is a silent false
|
||||
// negative, which is the direction that hurts.
|
||||
log.Printf("vulndb: compare %s %s vs %s: %v", p.Name, p.Version, a.FixedVersion, err)
|
||||
skipped++
|
||||
@@ -127,8 +127,8 @@ func Match(src AdvisorySource, os models.OSRelease, pkgs []models.InstalledPacka
|
||||
// necessary: Ubuntu encodes the ABI in the binary name, so an upgrade INSTALLS
|
||||
// linux-headers-6.8.0-137 beside linux-headers-6.8.0-124 rather than replacing
|
||||
// it, and the old one lingers until an autoremove. Matched per binary package,
|
||||
// a fully patched host reports every superseded ABI package as vulnerable —
|
||||
// which is the noise this exists to stop — and reports it twice over, once for
|
||||
// a fully patched host reports every superseded ABI package as vulnerable -
|
||||
// which is the noise this exists to stop - and reports it twice over, once for
|
||||
// linux-headers-6.8.0-124 and again for its -generic sibling.
|
||||
//
|
||||
// The version, not the name, decides. There is no kernel special case here: a
|
||||
@@ -176,7 +176,7 @@ func newestPerSource(family string, pkgs []models.InstalledPackage) []models.Ins
|
||||
// actionable decides whether an advisory with no fixed version is a finding.
|
||||
//
|
||||
// trivy-db fills Status only when FixedVersion is empty, and Ubuntu publishes a
|
||||
// status for every CVE against every source package it ships — the vast
|
||||
// status for every CVE against every source package it ships - the vast
|
||||
// majority being "under_investigation" (the tracker's needs-triage), meaning
|
||||
// nobody has yet established that the package is affected at all. Reporting
|
||||
// those produced ~24,000 findings for a single 797-package host, which is not a
|
||||
@@ -185,11 +185,11 @@ func newestPerSource(family string, pkgs []models.InstalledPackage) []models.Ins
|
||||
//
|
||||
// What survives is what the vendor has confirmed: affected, will_not_fix,
|
||||
// fix_deferred, end_of_life. Those are exactly the findings the CLAUDE.md rule
|
||||
// is about — an empty fixed_in that means "no fix exists", the one most in need
|
||||
// is about - an empty fixed_in that means "no fix exists", the one most in need
|
||||
// of acceptance rather than patching.
|
||||
// Only the two statuses that positively say "this is not a finding" are
|
||||
// dropped. "unknown" is kept: a feed that sets no status at all must not become
|
||||
// a silent false negative, and it is not what generates the noise — Ubuntu
|
||||
// a silent false negative, and it is not what generates the noise - Ubuntu
|
||||
// states under_investigation explicitly.
|
||||
func actionable(status string) bool {
|
||||
switch status {
|
||||
|
||||
@@ -25,7 +25,7 @@ const DefaultRef = "ghcr.io/aquasecurity/trivy-db:2"
|
||||
// SupportedSchema is the trivy-db schema version this code understands.
|
||||
//
|
||||
// A different version is refused rather than parsed on the assumption it is
|
||||
// close enough. Mis-reading the schema would not fail loudly — it would return
|
||||
// close enough. Mis-reading the schema would not fail loudly - it would return
|
||||
// no advisories, which is indistinguishable from a clean fleet.
|
||||
const SupportedSchema = 2
|
||||
|
||||
@@ -54,7 +54,7 @@ func Disabled() bool {
|
||||
// DebugEnabled turns on per-package and per-advisory tracing.
|
||||
//
|
||||
// It is a switch rather than always-on because a single scan asks the store one
|
||||
// question per installed package — ~2000 lines per server, per tick — which
|
||||
// question per installed package - ~2000 lines per server, per tick - which
|
||||
// would bury every other subsystem's logs on a fleet of any size. The lifecycle
|
||||
// logs (pull, tick, per-server totals) are unconditional; only the inner loop
|
||||
// is gated.
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
// epochs and sorts "~" before the empty string, rpmvercmp has its own segment
|
||||
// rules and treats "~" and "^" differently again, and any ordering that falls
|
||||
// back on string comparison puts 1.10 before 1.9. Every one of those mistakes
|
||||
// produces a false negative — a vulnerable host reported clean — which is the
|
||||
// produces a false negative - a vulnerable host reported clean - which is the
|
||||
// failure nobody notices.
|
||||
package vulndb
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ func (s *scheduler) tick(ctx context.Context) {
|
||||
}
|
||||
|
||||
// ensureDB pulls a fresh database when the local copy is stale, and marks the
|
||||
// whole fleet for rescanning when the version changes — which is what makes a
|
||||
// whole fleet for rescanning when the version changes - which is what makes a
|
||||
// newly published CVE flag existing servers within a minute rather than at the
|
||||
// next agent report.
|
||||
func (s *scheduler) ensureDB(ctx context.Context) error {
|
||||
@@ -188,7 +188,7 @@ func (s *scheduler) scanPending(ctx context.Context) {
|
||||
for _, sp := range pending {
|
||||
if ctx.Err() != nil {
|
||||
// Leadership lost. scan_pending is still set, so the next leader
|
||||
// picks these up — which is why it lives on the document.
|
||||
// picks these up - which is why it lives on the document.
|
||||
return
|
||||
}
|
||||
opened := s.scanOne(ctx, sp, meta)
|
||||
@@ -215,7 +215,7 @@ func (s *scheduler) scanOne(ctx context.Context, sp models.ServerPackages, meta
|
||||
results, err := vulndb.Match(s.store, sp.OS, sp.Packages)
|
||||
if err != nil {
|
||||
// We hold no feed for this distribution, so we cannot answer whether it
|
||||
// is vulnerable. Say "unsupported" — reporting zero findings here would
|
||||
// is vulnerable. Say "unsupported" - reporting zero findings here would
|
||||
// be indistinguishable from reporting a clean host, and one of those is
|
||||
// a lie.
|
||||
status := models.ScanStatusUnsupported
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Package workflowsched fires workflow runs on a cron schedule.
|
||||
//
|
||||
// Only robfig/cron's parser is used — Parse and Next. Its own scheduler is
|
||||
// Only robfig/cron's parser is used - Parse and Next. Its own scheduler is
|
||||
// not, because this work runs under the housekeeping leader lock and has to
|
||||
// stop the moment leadership is lost.
|
||||
package workflowsched
|
||||
@@ -14,12 +14,12 @@ import (
|
||||
)
|
||||
|
||||
// ErrBadSchedule covers both a malformed expression and an unknown timezone.
|
||||
// Handlers map it to 400 — both are the caller's mistake, and both are much
|
||||
// Handlers map it to 400 - both are the caller's mistake, and both are much
|
||||
// cheaper to find at save time than at 2am.
|
||||
var ErrBadSchedule = errors.New("invalid schedule")
|
||||
|
||||
// Standard 5-field cron: minute hour dom month dow. Deliberately no seconds
|
||||
// field and no descriptors — a schedule a person cannot read back is a
|
||||
// field and no descriptors - a schedule a person cannot read back is a
|
||||
// schedule nobody can audit.
|
||||
var cronParser = cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow)
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ func process(ctx context.Context, deps Deps, wf models.Workflow, now time.Time)
|
||||
|
||||
// The claim. Matching on the current next_run_at as well as the id means a
|
||||
// second process reaching this document after another has claimed it
|
||||
// matches nothing and does nothing. This — not the leader lock — is what
|
||||
// matches nothing and does nothing. This - not the leader lock - is what
|
||||
// makes a double fire impossible; the lock only keeps it cheap.
|
||||
res, err := db.Col("workflows").UpdateOne(ctx,
|
||||
bson.M{"workflow_id": wf.WorkflowID, "next_run_at": dueAt},
|
||||
@@ -107,7 +107,7 @@ func process(ctx context.Context, deps Deps, wf models.Workflow, now time.Time)
|
||||
case Fire:
|
||||
// nil tokenScope: the scheduler acts as the system, not as any user.
|
||||
// A schedule fires the workflow's own saved targets, and there is no
|
||||
// acting credential whose tag restriction could narrow them — the
|
||||
// acting credential whose tag restriction could narrow them - the
|
||||
// person who armed the schedule is not present at fire time, and
|
||||
// inheriting a restriction from whoever last saved the workflow would
|
||||
// make a run's reach depend on an editor's credential.
|
||||
|
||||
Reference in new issue
Block a user