@lamalibre/create-lamaste 4.2.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/scripts/bundle-vendor.js +13 -8
- package/scripts/lamaste-priv +205 -2
- package/src/index.js +8 -4
- package/src/lib/node-version.js +114 -0
- package/src/lib/panel-pin.js +41 -0
- package/src/lib/relay-services.js +15 -1
- package/src/lib/service-config.js +113 -0
- package/src/tasks/gatekeeper.js +8 -3
- package/src/tasks/node.js +34 -75
- package/src/tasks/redeploy.js +36 -0
- package/vendor/docs/00-introduction/how-it-works.md +2 -2
- package/vendor/docs/00-introduction/quickstart.md +5 -5
- package/vendor/docs/00-introduction/what-is-lamaste.md +5 -5
- package/vendor/docs/01-concepts/authentication.md +6 -3
- package/vendor/docs/01-concepts/authorization.md +29 -17
- package/vendor/docs/01-concepts/certificates.md +3 -3
- package/vendor/docs/01-concepts/mtls.md +2 -2
- package/vendor/docs/01-concepts/nginx-reverse-proxy.md +8 -3
- package/vendor/docs/01-concepts/security-model.md +110 -35
- package/vendor/docs/01-concepts/tickets.md +2 -2
- package/vendor/docs/01-concepts/tunneling.md +4 -4
- package/vendor/docs/02-guides/agent-setup.md +34 -16
- package/vendor/docs/02-guides/certificate-management.md +2 -2
- package/vendor/docs/02-guides/cloud-provisioning.md +4 -4
- package/vendor/docs/02-guides/desktop-app-setup.md +11 -10
- package/vendor/docs/02-guides/installation.md +3 -3
- package/vendor/docs/02-guides/onboarding.md +2 -2
- package/vendor/docs/02-guides/standalone-plugin-with-tickets.md +2 -2
- package/vendor/docs/03-architecture/installer-flow.md +2 -2
- package/vendor/docs/03-architecture/installer.md +4 -4
- package/vendor/docs/03-architecture/management-flow.md +1 -1
- package/vendor/docs/03-architecture/nginx-configuration.md +91 -26
- package/vendor/docs/03-architecture/overview.md +8 -8
- package/vendor/docs/03-architecture/panel-client.md +6 -6
- package/vendor/docs/03-architecture/panel-server.md +2 -2
- package/vendor/docs/03-architecture/state-management.md +2 -2
- package/vendor/docs/03-architecture/system-overview.md +1 -1
- package/vendor/docs/04-api-reference/agent-hosted-plugins.md +407 -0
- package/vendor/docs/04-api-reference/app-host.md +360 -0
- package/vendor/docs/04-api-reference/audit-and-notifications.md +57 -0
- package/vendor/docs/04-api-reference/certificates.md +95 -38
- package/vendor/docs/04-api-reference/gatekeeper.md +5 -3
- package/vendor/docs/04-api-reference/lifecycle-hooks.md +333 -0
- package/vendor/docs/04-api-reference/onboarding.md +1 -1
- package/vendor/docs/04-api-reference/overview.md +137 -111
- package/vendor/docs/04-api-reference/registry-and-oidc.md +8 -4
- package/vendor/docs/04-api-reference/tunnels.md +15 -13
- package/vendor/docs/05-operations/backup-and-restore.md +3 -1
- package/vendor/docs/05-operations/uninstalling.md +1 -1
- package/vendor/docs/05-operations/upgrades.md +25 -20
- package/vendor/docs/06-reference/config-files.md +195 -97
- package/vendor/docs/06-reference/glossary.md +3 -3
- package/vendor/docs/06-reference/installer-flags.md +13 -13
- package/vendor/docs/06-reference/ports-and-services.md +1 -1
- package/vendor/docs/06-reference/troubleshooting.md +33 -8
- package/vendor/docs/_index.json +20 -0
- package/vendor/docs/gatekeeper-design.md +22 -19
- package/vendor/docs/identity-directory-design.md +57 -56
- package/vendor/docs/package.json +1 -1
- package/vendor/docs/todo/lib-cli-desktop-refactor.md +27 -27
- package/vendor/gatekeeper/dist/cli/index.js +0 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts +12 -0
- package/vendor/gatekeeper/dist/lib/types.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.d.ts +2 -0
- package/vendor/gatekeeper/dist/server/index.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/index.js +47 -8
- package/vendor/gatekeeper/dist/server/index.js.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.d.ts.map +1 -1
- package/vendor/gatekeeper/dist/server/routes/authz.js +233 -141
- package/vendor/gatekeeper/dist/server/routes/authz.js.map +1 -1
- package/vendor/gatekeeper/package.json +4 -3
- package/vendor/server/bin/lamaste-server.js +25 -1
- package/vendor/server/package.json +2 -2
- package/vendor/server/src/commands/audit.js +60 -0
- package/vendor/server/src/commands/panel-pin.js +40 -0
- package/vendor/server/src/commands/plugins.js +963 -12
- package/vendor/server/src/commands/push-install.js +317 -0
- package/vendor/server/src/commands/registry-credential.js +2 -1
- package/vendor/server/src/commands/uninstall.js +28 -1
- package/vendor/server/src/ndjson.js +4 -2
- package/vendor/server/src/panel-api.js +277 -34
- package/vendor/server/src/panel-identity.js +165 -0
- package/vendor/server-ui/dist/assets/index-BTlBt_cD.js +84 -0
- package/vendor/server-ui/dist/assets/index-Bc5NCMSv.css +1 -0
- package/vendor/server-ui/dist/index.html +2 -2
- package/vendor/serverd/package.json +2 -2
- package/vendor/serverd/src/index.js +56 -4
- package/vendor/serverd/src/lib/actor.js +24 -0
- package/vendor/serverd/src/lib/agent-jobs.js +330 -0
- package/vendor/serverd/src/lib/app-error.js +10 -0
- package/vendor/serverd/src/lib/audit.js +145 -0
- package/vendor/serverd/src/lib/hook-effects.js +123 -0
- package/vendor/serverd/src/lib/hook-pipelines.js +902 -0
- package/vendor/serverd/src/lib/migrations/state/0006_plugin_agent.sql +4 -0
- package/vendor/serverd/src/lib/migrations/state/0007_audit_and_notifications.sql +29 -0
- package/vendor/serverd/src/lib/migrations/state/0008_plugin_hooks.sql +70 -0
- package/vendor/serverd/src/lib/migrations/state/0009_plugin_agent_half.sql +53 -0
- package/vendor/serverd/src/lib/migrations/state/0010_plugin_routes_and_retained_data.sql +47 -0
- package/vendor/serverd/src/lib/migrations/state/0011_agent_job_signature.sql +14 -0
- package/vendor/serverd/src/lib/mtls.js +89 -8
- package/vendor/serverd/src/lib/nginx.js +298 -44
- package/vendor/serverd/src/lib/notifications.js +107 -0
- package/vendor/serverd/src/lib/plugin-hooks.js +560 -0
- package/vendor/serverd/src/lib/plugin-hosting.js +688 -0
- package/vendor/serverd/src/lib/plugin-lifecycle.js +603 -0
- package/vendor/serverd/src/lib/plugin-operations.js +295 -0
- package/vendor/serverd/src/lib/plugin-packages.js +249 -0
- package/vendor/serverd/src/lib/plugins.js +298 -68
- package/vendor/serverd/src/lib/push-install.js +88 -2
- package/vendor/serverd/src/lib/relay-hooks.js +100 -0
- package/vendor/serverd/src/lib/relay-signing.js +43 -0
- package/vendor/serverd/src/lib/request-hooks.js +79 -0
- package/vendor/serverd/src/lib/retained-data.js +99 -0
- package/vendor/serverd/src/lib/trust-proxy.js +34 -0
- package/vendor/serverd/src/lib/tunnel-hooks.js +88 -0
- package/vendor/serverd/src/lib/vhost-reconcile.js +260 -0
- package/vendor/serverd/src/middleware/twofa-session.js +11 -1
- package/vendor/serverd/src/routes/agent-forwarder.js +37 -0
- package/vendor/serverd/src/routes/management/agent-panel-authz.js +97 -0
- package/vendor/serverd/src/routes/management/agents.js +262 -1
- package/vendor/serverd/src/routes/management/audit.js +81 -0
- package/vendor/serverd/src/routes/management/certs.js +1 -15
- package/vendor/serverd/src/routes/management/plugins.js +569 -54
- package/vendor/serverd/src/routes/management/tunnels.js +26 -0
- package/vendor/serverd/src/routes/management.js +4 -0
- package/vendor/server-ui/dist/assets/index-B5Wp8lu-.js +0 -257
- package/vendor/server-ui/dist/assets/index-BqWWFlTU.css +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after,::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border:0 solid #e5e7eb}:before,:after{--tw-content:""}html,:host{-webkit-text-size-adjust:100%;tab-size:4;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5}body{line-height:inherit;margin:0}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-feature-settings:normal;font-variation-settings:normal;font-family:JetBrains Mono,Fira Code,ui-monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-feature-settings:inherit;font-variation-settings:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:#0000;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{margin:0;padding:0;list-style:none}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder{opacity:1;color:#9ca3af}textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}body{--tw-bg-opacity:1;background-color:rgb(9 9 11/var(--tw-bg-opacity,1));--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.container{width:100%}@media (width>=640px){.container{max-width:640px}}@media (width>=768px){.container{max-width:768px}}@media (width>=1024px){.container{max-width:1024px}}@media (width>=1280px){.container{max-width:1280px}}@media (width>=1536px){.container{max-width:1536px}}.prose{color:var(--tw-prose-body);max-width:65ch}.prose :where(p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where([class~=lead]):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-lead);margin-top:1.2em;margin-bottom:1.2em;font-size:1.25em;line-height:1.6}.prose :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-links);font-weight:500;text-decoration:underline}.prose :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-bold);font-weight:600}.prose :where(a strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote strong):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:decimal}.prose :where(ol[type=A]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=A s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-alpha}.prose :where(ol[type=a s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-alpha}.prose :where(ol[type=I]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type=I s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:upper-roman}.prose :where(ol[type=i s]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:lower-roman}.prose :where(ol[type="1"]):not(:where([class~=not-prose],[class~=not-prose] *)){list-style-type:decimal}.prose :where(ul):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em;padding-inline-start:1.625em;list-style-type:disc}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-counters);font-weight:400}.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *))::marker{color:var(--tw-prose-bullets)}.prose :where(dt):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.25em;font-weight:600}.prose :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:var(--tw-prose-hr);border-top-width:1px;margin-top:3em;margin-bottom:3em}.prose :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-quotes);border-inline-start-width:.25rem;border-inline-start-color:var(--tw-prose-quote-borders);quotes:"“""”""‘""’";margin-top:1.6em;margin-bottom:1.6em;padding-inline-start:1em;font-style:italic;font-weight:500}.prose :where(blockquote p:first-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):before{content:open-quote}.prose :where(blockquote p:last-of-type):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:close-quote}.prose :where(h1):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:0;margin-bottom:.888889em;font-size:2.25em;font-weight:800;line-height:1.11111}.prose :where(h1 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:900}.prose :where(h2):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:2em;margin-bottom:1em;font-size:1.5em;font-weight:700;line-height:1.33333}.prose :where(h2 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:800}.prose :where(h3):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.6em;margin-bottom:.6em;font-size:1.25em;font-weight:600;line-height:1.6}.prose :where(h3 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(h4):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);margin-top:1.5em;margin-bottom:.5em;font-weight:600;line-height:1.5}.prose :where(h4 strong):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-weight:700}.prose :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(picture):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em;display:block}.prose :where(video):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(kbd):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-kbd);box-shadow:0 0 0 1px var(--tw-prose-kbd-shadows), 0 3px 0 var(--tw-prose-kbd-shadows);padding-top:.1875em;padding-inline-end:.375em;padding-bottom:.1875em;border-radius:.3125rem;padding-inline-start:.375em;font-family:inherit;font-size:.875em;font-weight:500}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-code);font-size:.875em;font-weight:600}.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:"`"}.prose :where(a code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h1 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(h2 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.875em}.prose :where(h3 code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit;font-size:.9em}.prose :where(h4 code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(blockquote code):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(thead th code):not(:where([class~=not-prose],[class~=not-prose] *)){color:inherit}.prose :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-pre-code);background-color:var(--tw-prose-pre-bg);padding-top:.857143em;padding-inline-end:1.14286em;padding-bottom:.857143em;border-radius:.375rem;margin-top:1.71429em;margin-bottom:1.71429em;padding-inline-start:1.14286em;font-size:.875em;font-weight:400;line-height:1.71429;overflow-x:auto}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)){font-weight:inherit;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit;background-color:#0000;border-width:0;border-radius:0;padding:0}.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose :where(pre code):not(:where([class~=not-prose],[class~=not-prose] *)):after{content:none}.prose :where(table):not(:where([class~=not-prose],[class~=not-prose] *)){table-layout:auto;width:100%;margin-top:2em;margin-bottom:2em;font-size:.875em;line-height:1.71429}.prose :where(thead):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-th-borders)}.prose :where(thead th):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-headings);vertical-align:bottom;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em;font-weight:600}.prose :where(tbody tr):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:1px;border-bottom-color:var(--tw-prose-td-borders)}.prose :where(tbody tr:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){border-bottom-width:0}.prose :where(tbody td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:baseline}.prose :where(tfoot):not(:where([class~=not-prose],[class~=not-prose] *)){border-top-width:1px;border-top-color:var(--tw-prose-th-borders)}.prose :where(tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){vertical-align:top}.prose :where(th,td):not(:where([class~=not-prose],[class~=not-prose] *)){text-align:start}.prose :where(figure>*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(figcaption):not(:where([class~=not-prose],[class~=not-prose] *)){color:var(--tw-prose-captions);margin-top:.857143em;font-size:.875em;line-height:1.42857}.prose{--tw-prose-body:#374151;--tw-prose-headings:#111827;--tw-prose-lead:#4b5563;--tw-prose-links:#111827;--tw-prose-bold:#111827;--tw-prose-counters:#6b7280;--tw-prose-bullets:#d1d5db;--tw-prose-hr:#e5e7eb;--tw-prose-quotes:#111827;--tw-prose-quote-borders:#e5e7eb;--tw-prose-captions:#6b7280;--tw-prose-kbd:#111827;--tw-prose-kbd-shadows:#1118271a;--tw-prose-code:#111827;--tw-prose-pre-code:#e5e7eb;--tw-prose-pre-bg:#1f2937;--tw-prose-th-borders:#d1d5db;--tw-prose-td-borders:#e5e7eb;--tw-prose-invert-body:#d1d5db;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#9ca3af;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#9ca3af;--tw-prose-invert-bullets:#4b5563;--tw-prose-invert-hr:#374151;--tw-prose-invert-quotes:#f3f4f6;--tw-prose-invert-quote-borders:#374151;--tw-prose-invert-captions:#9ca3af;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d1d5db;--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:#4b5563;--tw-prose-invert-td-borders:#374151;font-size:1rem;line-height:1.75}.prose :where(picture>img):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0;margin-bottom:0}.prose :where(li):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;margin-bottom:.5em}.prose :where(ol>li):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(ul>li):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:.375em}.prose :where(.prose>ul>li p):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(.prose>ul>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ul>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(.prose>ol>li>p:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em}.prose :where(.prose>ol>li>p:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:1.25em}.prose :where(ul ul,ul ol,ol ul,ol ol):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.75em;margin-bottom:.75em}.prose :where(dl):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:1.25em;margin-bottom:1.25em}.prose :where(dd):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:.5em;padding-inline-start:1.625em}.prose :where(hr+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h2+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h3+*):not(:where([class~=not-prose],[class~=not-prose] *)),.prose :where(h4+*):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(thead th:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(thead th:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(tbody td,tfoot td):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.571429em;padding-inline-end:.571429em;padding-bottom:.571429em;padding-inline-start:.571429em}.prose :where(tbody td:first-child,tfoot td:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-start:0}.prose :where(tbody td:last-child,tfoot td:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){padding-inline-end:0}.prose :where(figure):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:2em;margin-bottom:2em}.prose :where(.prose>:first-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-top:0}.prose :where(.prose>:last-child):not(:where([class~=not-prose],[class~=not-prose] *)){margin-bottom:0}.prose-zinc{--tw-prose-body:#3f3f46;--tw-prose-headings:#18181b;--tw-prose-lead:#52525b;--tw-prose-links:#18181b;--tw-prose-bold:#18181b;--tw-prose-counters:#71717a;--tw-prose-bullets:#d4d4d8;--tw-prose-hr:#e4e4e7;--tw-prose-quotes:#18181b;--tw-prose-quote-borders:#e4e4e7;--tw-prose-captions:#71717a;--tw-prose-kbd:#18181b;--tw-prose-kbd-shadows:#18181b1a;--tw-prose-code:#18181b;--tw-prose-pre-code:#e4e4e7;--tw-prose-pre-bg:#27272a;--tw-prose-th-borders:#d4d4d8;--tw-prose-td-borders:#e4e4e7;--tw-prose-invert-body:#d4d4d8;--tw-prose-invert-headings:#fff;--tw-prose-invert-lead:#a1a1aa;--tw-prose-invert-links:#fff;--tw-prose-invert-bold:#fff;--tw-prose-invert-counters:#a1a1aa;--tw-prose-invert-bullets:#52525b;--tw-prose-invert-hr:#3f3f46;--tw-prose-invert-quotes:#f4f4f5;--tw-prose-invert-quote-borders:#3f3f46;--tw-prose-invert-captions:#a1a1aa;--tw-prose-invert-kbd:#fff;--tw-prose-invert-kbd-shadows:#ffffff1a;--tw-prose-invert-code:#fff;--tw-prose-invert-pre-code:#d4d4d8;--tw-prose-invert-pre-bg:#00000080;--tw-prose-invert-th-borders:#52525b;--tw-prose-invert-td-borders:#3f3f46}.prose-invert{--tw-prose-body:var(--tw-prose-invert-body);--tw-prose-headings:var(--tw-prose-invert-headings);--tw-prose-lead:var(--tw-prose-invert-lead);--tw-prose-links:var(--tw-prose-invert-links);--tw-prose-bold:var(--tw-prose-invert-bold);--tw-prose-counters:var(--tw-prose-invert-counters);--tw-prose-bullets:var(--tw-prose-invert-bullets);--tw-prose-hr:var(--tw-prose-invert-hr);--tw-prose-quotes:var(--tw-prose-invert-quotes);--tw-prose-quote-borders:var(--tw-prose-invert-quote-borders);--tw-prose-captions:var(--tw-prose-invert-captions);--tw-prose-kbd:var(--tw-prose-invert-kbd);--tw-prose-kbd-shadows:var(--tw-prose-invert-kbd-shadows);--tw-prose-code:var(--tw-prose-invert-code);--tw-prose-pre-code:var(--tw-prose-invert-pre-code);--tw-prose-pre-bg:var(--tw-prose-invert-pre-bg);--tw-prose-th-borders:var(--tw-prose-invert-th-borders);--tw-prose-td-borders:var(--tw-prose-invert-td-borders)}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.bottom-4{bottom:1rem}.left-4{left:1rem}.right-3{right:.75rem}.right-4{right:1rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.top-6{top:1.5rem}.z-40{z-index:40}.z-50{z-index:50}.m-4{margin:1rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.-ml-px{margin-left:-1px}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-4{margin-right:1rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-12{margin-top:3rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-36{margin-top:9rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-24{height:6rem}.h-3{height:.75rem}.h-32{height:8rem}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-96{height:24rem}.h-screen{height:100vh}.max-h-40{max-height:10rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-\[90vh\]{max-height:90vh}.max-h-full{max-height:100%}.min-h-64{min-height:16rem}.min-h-screen{min-height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-28{width:7rem}.w-3{width:.75rem}.w-36{width:9rem}.w-4{width:1rem}.w-40{width:10rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-72{width:18rem}.w-8{width:2rem}.w-full{width:100%}.w-px{width:1px}.min-w-0{min-width:0}.min-w-\[16rem\]{min-width:16rem}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.max-w-xl{max-width:36rem}.flex-1{flex:1}.flex-shrink-0,.shrink-0{flex-shrink:0}.basis-full{flex-basis:100%}.-translate-y-1\/2{--tw-translate-y:-50%;transform:translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes fade-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.animate-fade-in{animation:.5s ease-out fade-in}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:2s cubic-bezier(.4,0,.6,1) infinite pulse}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:1s linear infinite spin}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.select-all{-webkit-user-select:all;user-select:all}.scroll-mt-6{scroll-margin-top:1.5rem}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0{gap:0}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.gap-8{gap:2rem}.gap-x-1\.5{-moz-column-gap:.375rem;column-gap:.375rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-1{row-gap:.25rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.125rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.125rem * var(--tw-space-y-reverse))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-b-lg{border-bottom-right-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-0{border-left-width:0}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-t-0{border-top-width:0}.border-amber-500\/20{border-color:#f59e0b33}.border-amber-500\/30{border-color:#f59e0b4d}.border-amber-700{--tw-border-opacity:1;border-color:rgb(180 83 9/var(--tw-border-opacity,1))}.border-cyan-400{--tw-border-opacity:1;border-color:rgb(34 211 238/var(--tw-border-opacity,1))}.border-cyan-500\/20{border-color:#06b6d433}.border-cyan-500\/30{border-color:#06b6d44d}.border-cyan-500\/60{border-color:#06b6d499}.border-emerald-500\/20{border-color:#10b98133}.border-green-500\/20{border-color:#22c55e33}.border-purple-500\/20{border-color:#a855f733}.border-red-500\/20{border-color:#ef444433}.border-red-500\/30{border-color:#ef44444d}.border-red-500\/70{border-color:#ef4444b3}.border-red-700{--tw-border-opacity:1;border-color:rgb(185 28 28/var(--tw-border-opacity,1))}.border-transparent{border-color:#0000}.border-violet-500\/20{border-color:#8b5cf633}.border-yellow-500\/20{border-color:#eab30833}.border-yellow-500\/30{border-color:#eab3084d}.border-zinc-600{--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1))}.border-zinc-700{--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1))}.border-zinc-800{--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.border-zinc-800\/50{border-color:#27272a80}.border-t-cyan-400{--tw-border-opacity:1;border-top-color:rgb(34 211 238/var(--tw-border-opacity,1))}.bg-amber-400{--tw-bg-opacity:1;background-color:rgb(251 191 36/var(--tw-bg-opacity,1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-500\/20{background-color:#f59e0b33}.bg-amber-500\/5{background-color:#f59e0b0d}.bg-amber-600{--tw-bg-opacity:1;background-color:rgb(217 119 6/var(--tw-bg-opacity,1))}.bg-amber-900\/30{background-color:#78350f4d}.bg-black\/50{background-color:#00000080}.bg-black\/60{background-color:#0009}.bg-black\/70{background-color:#000000b3}.bg-blue-500\/10{background-color:#3b82f61a}.bg-blue-500\/20{background-color:#3b82f633}.bg-cyan-400{--tw-bg-opacity:1;background-color:rgb(34 211 238/var(--tw-bg-opacity,1))}.bg-cyan-500{--tw-bg-opacity:1;background-color:rgb(6 182 212/var(--tw-bg-opacity,1))}.bg-cyan-500\/10{background-color:#06b6d41a}.bg-cyan-500\/20{background-color:#06b6d433}.bg-cyan-500\/5{background-color:#06b6d40d}.bg-cyan-600{--tw-bg-opacity:1;background-color:rgb(8 145 178/var(--tw-bg-opacity,1))}.bg-emerald-500\/10{background-color:#10b9811a}.bg-emerald-600{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity,1))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:#22c55e1a}.bg-green-500\/15{background-color:#22c55e26}.bg-green-500\/20{background-color:#22c55e33}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(22 163 74/var(--tw-bg-opacity,1))}.bg-orange-500\/10{background-color:#f973161a}.bg-orange-500\/20{background-color:#f9731633}.bg-purple-500\/10{background-color:#a855f71a}.bg-purple-500\/20{background-color:#a855f733}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-500\/20{background-color:#ef444433}.bg-red-500\/5{background-color:#ef44440d}.bg-red-600{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.bg-red-600\/20{background-color:#dc262633}.bg-red-900\/30{background-color:#7f1d1d4d}.bg-transparent{background-color:#0000}.bg-violet-500\/10{background-color:#8b5cf61a}.bg-yellow-500\/10{background-color:#eab3081a}.bg-yellow-500\/15{background-color:#eab30826}.bg-yellow-500\/20{background-color:#eab30833}.bg-yellow-500\/5{background-color:#eab3080d}.bg-zinc-500{--tw-bg-opacity:1;background-color:rgb(113 113 122/var(--tw-bg-opacity,1))}.bg-zinc-500\/20{background-color:#71717a33}.bg-zinc-600{--tw-bg-opacity:1;background-color:rgb(82 82 91/var(--tw-bg-opacity,1))}.bg-zinc-700{--tw-bg-opacity:1;background-color:rgb(63 63 70/var(--tw-bg-opacity,1))}.bg-zinc-700\/60{background-color:#3f3f4699}.bg-zinc-800{--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1))}.bg-zinc-800\/20{background-color:#27272a33}.bg-zinc-800\/50{background-color:#27272a80}.bg-zinc-800\/60{background-color:#27272a99}.bg-zinc-900{--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity,1))}.bg-zinc-900\/50{background-color:#18181b80}.bg-zinc-950{--tw-bg-opacity:1;background-color:rgb(9 9 11/var(--tw-bg-opacity,1))}.bg-zinc-950\/40{background-color:#09090b66}.bg-zinc-950\/60{background-color:#09090b99}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-12{padding:3rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pb-4{padding-bottom:1rem}.pl-1{padding-left:.25rem}.pl-3{padding-left:.75rem}.pl-4{padding-left:1rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pr-10{padding-right:2.5rem}.pr-3{padding-right:.75rem}.pr-4{padding-right:1rem}.pt-2{padding-top:.5rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:JetBrains Mono,Fira Code,ui-monospace,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-\[0\.2em\]{letter-spacing:.2em}.tracking-\[0\.3em\]{letter-spacing:.3em}.tracking-wider{letter-spacing:.05em}.text-amber-300{--tw-text-opacity:1;color:rgb(252 211 77/var(--tw-text-opacity,1))}.text-amber-400{--tw-text-opacity:1;color:rgb(251 191 36/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-cyan-500{--tw-text-opacity:1;color:rgb(6 182 212/var(--tw-text-opacity,1))}.text-emerald-400{--tw-text-opacity:1;color:rgb(52 211 153/var(--tw-text-opacity,1))}.text-emerald-400\/70{color:#34d399b3}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-violet-400{--tw-text-opacity:1;color:rgb(167 139 250/var(--tw-text-opacity,1))}.text-violet-400\/70{color:#a78bfab3}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-400\/90{color:#facc15e6}.text-yellow-500\/80{color:#eab308cc}.text-zinc-100{--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.text-zinc-200{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.text-zinc-300{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.text-zinc-400{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.text-zinc-500{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.text-zinc-900{--tw-text-opacity:1;color:rgb(24 24 27/var(--tw-text-opacity,1))}.placeholder-zinc-500::placeholder{--tw-placeholder-opacity:1;color:rgb(113 113 122/var(--tw-placeholder-opacity,1))}.placeholder-zinc-600::placeholder{--tw-placeholder-opacity:1;color:rgb(82 82 91/var(--tw-placeholder-opacity,1))}.accent-cyan-500{accent-color:#06b6d4}.opacity-60{opacity:.6}.opacity-80{opacity:.8}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a, 0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000), var(--tw-ring-shadow,0 0 #0000), var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-duration:.15s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.placeholder\:text-zinc-600::placeholder{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.last\:border-b-0:last-child{border-bottom-width:0}.hover\:border-cyan-500:hover{--tw-border-opacity:1;border-color:rgb(6 182 212/var(--tw-border-opacity,1))}.hover\:border-cyan-600:hover{--tw-border-opacity:1;border-color:rgb(8 145 178/var(--tw-border-opacity,1))}.hover\:border-zinc-600:hover{--tw-border-opacity:1;border-color:rgb(82 82 91/var(--tw-border-opacity,1))}.hover\:border-zinc-700:hover{--tw-border-opacity:1;border-color:rgb(63 63 70/var(--tw-border-opacity,1))}.hover\:bg-amber-500:hover{--tw-bg-opacity:1;background-color:rgb(245 158 11/var(--tw-bg-opacity,1))}.hover\:bg-cyan-300:hover{--tw-bg-opacity:1;background-color:rgb(103 232 249/var(--tw-bg-opacity,1))}.hover\:bg-cyan-500:hover{--tw-bg-opacity:1;background-color:rgb(6 182 212/var(--tw-bg-opacity,1))}.hover\:bg-cyan-600:hover{--tw-bg-opacity:1;background-color:rgb(8 145 178/var(--tw-bg-opacity,1))}.hover\:bg-cyan-600\/20:hover{background-color:#0891b233}.hover\:bg-emerald-500:hover{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.hover\:bg-green-500:hover{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(21 128 61/var(--tw-bg-opacity,1))}.hover\:bg-red-500:hover{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.hover\:bg-red-500\/20:hover{background-color:#ef444433}.hover\:bg-red-600\/20:hover{background-color:#dc262633}.hover\:bg-red-600\/30:hover{background-color:#dc26264d}.hover\:bg-yellow-600\/20:hover{background-color:#ca8a0433}.hover\:bg-zinc-500:hover{--tw-bg-opacity:1;background-color:rgb(113 113 122/var(--tw-bg-opacity,1))}.hover\:bg-zinc-600:hover{--tw-bg-opacity:1;background-color:rgb(82 82 91/var(--tw-bg-opacity,1))}.hover\:bg-zinc-700:hover{--tw-bg-opacity:1;background-color:rgb(63 63 70/var(--tw-bg-opacity,1))}.hover\:bg-zinc-800:hover{--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1))}.hover\:bg-zinc-800\/30:hover{background-color:#27272a4d}.hover\:bg-zinc-800\/50:hover{background-color:#27272a80}.hover\:text-cyan-300:hover{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.hover\:text-cyan-400:hover{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-red-400:hover{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:text-yellow-300:hover{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.hover\:text-zinc-100:hover{--tw-text-opacity:1;color:rgb(244 244 245/var(--tw-text-opacity,1))}.hover\:text-zinc-200:hover{--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.hover\:text-zinc-300:hover{--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.hover\:text-zinc-400:hover{--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-cyan-400:focus{--tw-border-opacity:1;border-color:rgb(34 211 238/var(--tw-border-opacity,1))}.focus\:border-cyan-500:focus{--tw-border-opacity:1;border-color:rgb(6 182 212/var(--tw-border-opacity,1))}.focus\:outline-none:focus{outline-offset:2px;outline:2px solid #0000}.focus\:ring-1:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow,0 0 #0000)}.focus\:ring-cyan-400:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(34 211 238/var(--tw-ring-opacity,1))}.focus\:ring-cyan-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(6 182 212/var(--tw-ring-opacity,1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:text-zinc-500:disabled{--tw-text-opacity:1;color:rgb(113 113 122/var(--tw-text-opacity,1))}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.prose-headings\:scroll-mt-20 :where(h1,h2,h3,h4,h5,h6,th):not(:where([class~=not-prose],[class~=not-prose] *)){scroll-margin-top:5rem}.prose-headings\:font-mono :where(h1,h2,h3,h4,h5,h6,th):not(:where([class~=not-prose],[class~=not-prose] *)){font-family:JetBrains Mono,Fira Code,ui-monospace,monospace}.prose-a\:text-cyan-400 :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.prose-a\:no-underline :where(a):not(:where([class~=not-prose],[class~=not-prose] *)){text-decoration-line:none}.hover\:prose-a\:underline :where(a):not(:where([class~=not-prose],[class~=not-prose] *)):hover{text-decoration-line:underline}.prose-blockquote\:border-cyan-400\/30 :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){border-color:#22d3ee4d}.prose-blockquote\:text-zinc-400 :where(blockquote):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.prose-strong\:text-zinc-200 :where(strong):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(228 228 231/var(--tw-text-opacity,1))}.prose-code\:rounded :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.25rem}.prose-code\:bg-zinc-800 :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-bg-opacity:1;background-color:rgb(39 39 42/var(--tw-bg-opacity,1))}.prose-code\:px-1\.5 :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){padding-left:.375rem;padding-right:.375rem}.prose-code\:py-0\.5 :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){padding-top:.125rem;padding-bottom:.125rem}.prose-code\:text-cyan-300 :where(code):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.prose-code\:before\:content-none :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):before,.prose-code\:after\:content-none :where(code):not(:where([class~=not-prose],[class~=not-prose] *)):after{--tw-content:none;content:var(--tw-content)}.prose-pre\:border :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){border-width:1px}.prose-pre\:border-zinc-800 :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}.prose-pre\:bg-zinc-900 :where(pre):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-bg-opacity:1;background-color:rgb(24 24 27/var(--tw-bg-opacity,1))}.prose-th\:text-zinc-300 :where(th):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(212 212 216/var(--tw-text-opacity,1))}.prose-td\:text-zinc-400 :where(td):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-text-opacity:1;color:rgb(161 161 170/var(--tw-text-opacity,1))}.prose-img\:rounded-lg :where(img):not(:where([class~=not-prose],[class~=not-prose] *)){border-radius:.5rem}.prose-hr\:border-zinc-800 :where(hr):not(:where([class~=not-prose],[class~=not-prose] *)){--tw-border-opacity:1;border-color:rgb(39 39 42/var(--tw-border-opacity,1))}@media (width>=640px){.sm\:table-cell{display:table-cell}}@media (width>=768px){.md\:block{display:block}.md\:table-cell{display:table-cell}.md\:hidden{display:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (width>=1024px){.lg\:flex{display:flex}.lg\:table-cell{display:table-cell}.lg\:hidden{display:none}.lg\:p-8{padding:2rem}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media (width>=1280px){.xl\:block{display:block}}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Lamaste</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-BTlBt_cD.js"></script>
|
|
8
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bc5NCMSv.css">
|
|
9
9
|
</head>
|
|
10
10
|
<body style="background-color: #09090b">
|
|
11
11
|
<div id="root"></div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lamalibre/lamaste-serverd",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.1",
|
|
4
4
|
"description": "Lamaste server daemon — REST API and plugin host",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"publishConfig": {
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"@fastify/rate-limit": "^10.3.0",
|
|
30
30
|
"@fastify/static": "^9.1.1",
|
|
31
31
|
"@fastify/websocket": "^11.0.0",
|
|
32
|
-
"@lamalibre/lamaste": "^4.
|
|
32
|
+
"@lamalibre/lamaste": "^4.3.1",
|
|
33
33
|
"bcryptjs": "^3.0.3",
|
|
34
34
|
"execa": "^9.6.1",
|
|
35
35
|
"fastify": "^5.8.4",
|
|
@@ -17,6 +17,7 @@ import healthRoutes from './routes/health.js';
|
|
|
17
17
|
import onboardingRoutes from './routes/onboarding/index.js';
|
|
18
18
|
import managementRoutes from './routes/management.js';
|
|
19
19
|
import pluginRouter from './routes/plugin-router.js';
|
|
20
|
+
import agentForwarderRoutes from './routes/agent-forwarder.js';
|
|
20
21
|
import inviteRoutes from './routes/invite.js';
|
|
21
22
|
import enrollmentRoutes from './routes/enrollment.js';
|
|
22
23
|
import {
|
|
@@ -25,10 +26,20 @@ import {
|
|
|
25
26
|
userAccessProtectedRoutes,
|
|
26
27
|
} from './routes/user-access.js';
|
|
27
28
|
import userAccessSessionMiddleware from './middleware/user-access-session.js';
|
|
28
|
-
import { getPluginCapabilities } from './lib/plugins.js';
|
|
29
|
+
import { getPluginCapabilities, readPlugins } from './lib/plugins.js';
|
|
30
|
+
import { markInterruptedOperations } from './lib/plugin-operations.js';
|
|
31
|
+
import { trustLocalNginxHop } from './lib/trust-proxy.js';
|
|
32
|
+
import { reconcileRelayHooks } from './lib/plugin-hooks.js';
|
|
33
|
+
import { loadTunnelFallbacks } from './lib/request-hooks.js';
|
|
29
34
|
import { setPluginCapabilities } from './lib/mtls.js';
|
|
30
35
|
import { startChiselReconciler } from './lib/chisel-reconcile.js';
|
|
36
|
+
import {
|
|
37
|
+
startHostedPluginHealthMonitor,
|
|
38
|
+
stopHostedPluginHealthMonitor,
|
|
39
|
+
} from './lib/plugin-hosting.js';
|
|
31
40
|
import { syncOidcClients } from './lib/plugin-oidc.js';
|
|
41
|
+
import { ensureRelaySigningKey } from './lib/relay-signing.js';
|
|
42
|
+
import { reconcileSiteVhosts, reconcileTunnelVhosts } from './lib/vhost-reconcile.js';
|
|
32
43
|
import {
|
|
33
44
|
loadTicketScopeCapabilities,
|
|
34
45
|
checkInstanceLiveness,
|
|
@@ -61,9 +72,10 @@ async function start() {
|
|
|
61
72
|
|
|
62
73
|
const server = Fastify({
|
|
63
74
|
logger: true,
|
|
64
|
-
// Behind nginx: trust exactly one
|
|
65
|
-
//
|
|
66
|
-
|
|
75
|
+
// Behind the local nginx: trust exactly that one loopback hop, so
|
|
76
|
+
// request.ip is the client address nginx appended to X-Forwarded-For —
|
|
77
|
+
// not a value the client sent, and nothing from a non-local peer.
|
|
78
|
+
trustProxy: trustLocalNginxHop,
|
|
67
79
|
});
|
|
68
80
|
|
|
69
81
|
// Wire ticket module to the Fastify root logger so any load-time warnings
|
|
@@ -87,6 +99,11 @@ async function start() {
|
|
|
87
99
|
});
|
|
88
100
|
}
|
|
89
101
|
|
|
102
|
+
// The key relay assertions to agents' lamaste-agentd are signed with —
|
|
103
|
+
// created here when missing, before Gatekeeper (which signs with it too)
|
|
104
|
+
// needs it.
|
|
105
|
+
await ensureRelaySigningKey(server.log);
|
|
106
|
+
|
|
90
107
|
// --- Plugins ---
|
|
91
108
|
const ipOrigin = `https://${config.ip}:9292`;
|
|
92
109
|
await server.register(cors, {
|
|
@@ -158,6 +175,9 @@ async function start() {
|
|
|
158
175
|
app.register(managementRoutes, { prefix: '/api' });
|
|
159
176
|
app.register(userAccessAdminRoutes, { prefix: '/api' });
|
|
160
177
|
app.register(pluginRouter, { prefix: '/api' });
|
|
178
|
+
// Last: agent-hosted plugins with an agent half, forwarded over their
|
|
179
|
+
// internal tunnels. Only what no other /api route answers reaches it.
|
|
180
|
+
app.register(agentForwarderRoutes, { prefix: '/api' });
|
|
161
181
|
});
|
|
162
182
|
|
|
163
183
|
// --- User-access session-protected routes (Bearer token, no mTLS) ---
|
|
@@ -176,8 +196,39 @@ async function start() {
|
|
|
176
196
|
});
|
|
177
197
|
|
|
178
198
|
// --- Start ---
|
|
199
|
+
|
|
200
|
+
// The auth portal and static site vhosts an older version wrote name the
|
|
201
|
+
// client's Host header, not their own hostname, to Authelia. Before listen:
|
|
202
|
+
// site changes take no lock, and none can arrive until the panel listens.
|
|
203
|
+
if (config.onboarding.status === 'COMPLETED' && config.domain) {
|
|
204
|
+
await reconcileSiteVhosts(server.log, config.domain).catch((err) => {
|
|
205
|
+
server.log.error({ err: err.message }, 'Startup site vhost reconcile failed');
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
179
209
|
await server.listen({ host: '127.0.0.1', port: 3100 });
|
|
180
210
|
|
|
211
|
+
// --- Lifecycle hooks: operations a restart cut short, the runners of
|
|
212
|
+
// approved relay hook sets (a redeploy may have reset them), fallbacks ---
|
|
213
|
+
await markInterruptedOperations(server.log);
|
|
214
|
+
await loadTunnelFallbacks();
|
|
215
|
+
readPlugins()
|
|
216
|
+
.then(({ plugins }) => reconcileRelayHooks(plugins, server.log))
|
|
217
|
+
.catch((err) => server.log.error({ err: err.message }, 'Relay hook reconciliation failed'));
|
|
218
|
+
|
|
219
|
+
// Tunnel vhosts written by an older version: agent-panel and plugin vhosts
|
|
220
|
+
// do not forward relay assertions, without which lamaste-agentd refuses the
|
|
221
|
+
// relay, and Gatekeeper vhosts name the client's Host header to Gatekeeper.
|
|
222
|
+
// After listen: a rewritten agent-panel vhost asks this server at once.
|
|
223
|
+
if (config.onboarding.status === 'COMPLETED') {
|
|
224
|
+
reconcileTunnelVhosts(server.log).catch((err) => {
|
|
225
|
+
server.log.error({ err: err.message }, 'Startup tunnel vhost reconcile failed');
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// --- Health of hosted agent-hosted plugins (every 10s; idle without hostings) ---
|
|
230
|
+
startHostedPluginHealthMonitor(server.log);
|
|
231
|
+
|
|
181
232
|
// --- Periodic ticket instance liveness check (every 60s) ---
|
|
182
233
|
const livenessInterval = setInterval(() => {
|
|
183
234
|
checkInstanceLiveness(server.log).catch((err) => {
|
|
@@ -190,6 +241,7 @@ async function start() {
|
|
|
190
241
|
const shutdown = async (signal) => {
|
|
191
242
|
server.log.info({ signal }, 'Received signal, shutting down gracefully');
|
|
192
243
|
clearInterval(livenessInterval);
|
|
244
|
+
stopHostedPluginHealthMonitor();
|
|
193
245
|
clearRateLimitInterval();
|
|
194
246
|
await server.close();
|
|
195
247
|
process.exit(0);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a stable audit identifier for the calling principal.
|
|
3
|
+
*
|
|
4
|
+
* Returns the certificate CN form (`admin`, `agent:<label>`, `plugin-agent:...`)
|
|
5
|
+
* so a reader of the journal or the audit log can correlate events with the
|
|
6
|
+
* principal that initiated them. Returns `'unknown'` when middleware did not
|
|
7
|
+
* populate cert metadata (e.g. dev mode shortcut on /api/health).
|
|
8
|
+
*/
|
|
9
|
+
export function actorFromRequest(request) {
|
|
10
|
+
if (request.certRole === 'admin') return 'admin';
|
|
11
|
+
if (request.certRole === 'agent' && request.certLabel) return `agent:${request.certLabel}`;
|
|
12
|
+
if (request.certRole === 'plugin-agent' && request.certLabel) return request.certLabel;
|
|
13
|
+
return 'unknown';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The client certificate serial nginx reported, for audit entries: every
|
|
18
|
+
* administrator shares the `admin` identity, so the serial is what tells two
|
|
19
|
+
* admin certificates apart. Not secret. Null when absent (development mode).
|
|
20
|
+
*/
|
|
21
|
+
export function certSerialFromRequest(request) {
|
|
22
|
+
const serial = request.headers?.['x-ssl-client-serial'];
|
|
23
|
+
return typeof serial === 'string' && /^[0-9A-Fa-f]{1,64}$/.test(serial) ? serial : null;
|
|
24
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent jobs — the panel's way to have an agent do something on its own
|
|
3
|
+
* machine and report back. The panel cannot reach an agent (agents connect
|
|
4
|
+
* out, behind NAT); an agent's sync timer fetches `agent-config` every 30 s,
|
|
5
|
+
* which lists the agent's pending jobs, runs them, and posts each result to
|
|
6
|
+
* `POST /api/agents/me/jobs/:id/result`.
|
|
7
|
+
*
|
|
8
|
+
* The kinds (core `agent-jobs.ts` has the result contract both ends check):
|
|
9
|
+
*
|
|
10
|
+
* - `hook` — call the agent half's `POST /admin/hooks/<stage>` on the
|
|
11
|
+
* agent's loopback, for the lifecycle stages that run while the internal
|
|
12
|
+
* tunnel is not there (the agent's `install:pre-tunnel` before it is
|
|
13
|
+
* granted, `uninstall:post-teardown` after it is removed). The agent
|
|
14
|
+
* signs that call with the installation secret it already holds.
|
|
15
|
+
* - `app-install` (`spec`: the pinned agent half), `app-activate`
|
|
16
|
+
* (`version`, `package`) and `app-remove` (`purge`, `package`) — install,
|
|
17
|
+
* switch or remove the agent half through the agent's root helper
|
|
18
|
+
* `lamaste-apphost`. The agent fetches the package itself and installs it
|
|
19
|
+
* only when its tarball matches the pinned integrity.
|
|
20
|
+
*
|
|
21
|
+
* The helper runs as root and does only what the relay signed: every app
|
|
22
|
+
* job is signed with the relay's signing key when it is created (core
|
|
23
|
+
* `app-job-signature.ts`; the envelope names the agent, route, job id, kind
|
|
24
|
+
* and payload, and expires at the job's deadline) and listed with its
|
|
25
|
+
* signature. No app job is created while the key is unusable.
|
|
26
|
+
*
|
|
27
|
+
* A job carries no secret. It has a deadline (its timeout plus delivery
|
|
28
|
+
* slack); an unfinished job past it expires. Waiting is event-driven within
|
|
29
|
+
* this process, with the database as the record.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import crypto from 'node:crypto';
|
|
33
|
+
import { EventEmitter } from 'node:events';
|
|
34
|
+
import {
|
|
35
|
+
AGENT_APP_JOB_TIMEOUTS,
|
|
36
|
+
AgentAppJobResultSchema,
|
|
37
|
+
AgentHookJobResultSchema,
|
|
38
|
+
appJobPayload,
|
|
39
|
+
signAppJob,
|
|
40
|
+
} from '@lamalibre/lamaste';
|
|
41
|
+
import { relaySigner } from './relay-signing.js';
|
|
42
|
+
import { getStateDb } from './state-db.js';
|
|
43
|
+
|
|
44
|
+
/** Two sync intervals and a margin: how long delivery may take on top of the hook's own timeout. */
|
|
45
|
+
export const AGENT_JOB_DELIVERY_SLACK_MS = 90_000;
|
|
46
|
+
|
|
47
|
+
/** Largest result an agent may post. */
|
|
48
|
+
export const AGENT_JOB_RESULT_MAX_BYTES = 64 * 1024;
|
|
49
|
+
|
|
50
|
+
/** The app job kinds and how long the agent has for each (the helper's work, not delivery). */
|
|
51
|
+
export const AGENT_APP_JOB_KINDS = Object.freeze({
|
|
52
|
+
'app-install': AGENT_APP_JOB_TIMEOUTS.install,
|
|
53
|
+
'app-activate': AGENT_APP_JOB_TIMEOUTS.activate,
|
|
54
|
+
'app-remove': AGENT_APP_JOB_TIMEOUTS.remove,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const events = new EventEmitter();
|
|
58
|
+
events.setMaxListeners(0);
|
|
59
|
+
|
|
60
|
+
/** An app job could not be signed: the relay's signing key is missing or unusable. */
|
|
61
|
+
export class AppJobSigningError extends Error {
|
|
62
|
+
constructor(message) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = 'AppJobSigningError';
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function rowToJob(row) {
|
|
69
|
+
return {
|
|
70
|
+
id: row.id,
|
|
71
|
+
agentLabel: row.agent_label,
|
|
72
|
+
route: row.plugin,
|
|
73
|
+
kind: row.kind,
|
|
74
|
+
stage: row.stage,
|
|
75
|
+
input: row.input ? JSON.parse(row.input) : null,
|
|
76
|
+
payload: row.payload ? JSON.parse(row.payload) : null,
|
|
77
|
+
signature: row.signature ? JSON.parse(row.signature) : null,
|
|
78
|
+
timeoutMs: row.timeout_ms,
|
|
79
|
+
status: row.status,
|
|
80
|
+
result: row.result ? JSON.parse(row.result) : null,
|
|
81
|
+
createdAt: row.created_at,
|
|
82
|
+
deadlineAt: row.deadline_at,
|
|
83
|
+
completedAt: row.completed_at,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Insert a pending job: `stage` and `input` for a hook job, `payload` (and
|
|
89
|
+
* `sign`, which makes its signature) for an app job; null otherwise. Its
|
|
90
|
+
* deadline leaves delivery {@link AGENT_JOB_DELIVERY_SLACK_MS} on top of
|
|
91
|
+
* `timeoutMs`.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} job
|
|
94
|
+
* @param {((timing: { id: string, issuedAt: string, expiresAt: string }) => object) | null} [job.sign]
|
|
95
|
+
* @returns {Promise<string>} the job id
|
|
96
|
+
*/
|
|
97
|
+
async function insertJob({ agentLabel, plugin, kind, stage, input, payload, timeoutMs, sign }) {
|
|
98
|
+
const db = await getStateDb();
|
|
99
|
+
const id = crypto.randomUUID();
|
|
100
|
+
const now = Date.now();
|
|
101
|
+
const createdAt = new Date(now).toISOString();
|
|
102
|
+
const deadlineAt = new Date(now + timeoutMs + AGENT_JOB_DELIVERY_SLACK_MS).toISOString();
|
|
103
|
+
const signature = sign ? sign({ id, issuedAt: createdAt, expiresAt: deadlineAt }) : null;
|
|
104
|
+
db.prepare(
|
|
105
|
+
`INSERT INTO agent_jobs (id, agent_label, plugin, kind, stage, input, payload, signature, timeout_ms, status, created_at, deadline_at)
|
|
106
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?)`,
|
|
107
|
+
).run(
|
|
108
|
+
id,
|
|
109
|
+
agentLabel,
|
|
110
|
+
plugin,
|
|
111
|
+
kind,
|
|
112
|
+
stage,
|
|
113
|
+
input,
|
|
114
|
+
payload,
|
|
115
|
+
signature === null ? null : JSON.stringify(signature),
|
|
116
|
+
timeoutMs,
|
|
117
|
+
createdAt,
|
|
118
|
+
deadlineAt,
|
|
119
|
+
);
|
|
120
|
+
return id;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A `hook` job: call the agent half's `stage` hook on the agent's loopback.
|
|
125
|
+
*
|
|
126
|
+
* @param {{ agentLabel: string, plugin: string, stage: string, input: object, timeoutMs: number }} job
|
|
127
|
+
* @returns {Promise<string>} the job id
|
|
128
|
+
*/
|
|
129
|
+
export async function createAgentJob({ agentLabel, plugin, stage, input, timeoutMs }) {
|
|
130
|
+
return insertJob({
|
|
131
|
+
agentLabel,
|
|
132
|
+
plugin,
|
|
133
|
+
kind: 'hook',
|
|
134
|
+
stage,
|
|
135
|
+
input: JSON.stringify(input),
|
|
136
|
+
payload: null,
|
|
137
|
+
timeoutMs,
|
|
138
|
+
sign: null,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The relay's signing key for an app job; throws {@link AppJobSigningError}
|
|
144
|
+
* when there is none to use — the agent's helper would refuse the job.
|
|
145
|
+
*/
|
|
146
|
+
async function appJobSigningKey() {
|
|
147
|
+
let key;
|
|
148
|
+
try {
|
|
149
|
+
key = await relaySigner.current();
|
|
150
|
+
} catch (err) {
|
|
151
|
+
throw new AppJobSigningError(
|
|
152
|
+
`The relay's signing key is unusable (${err instanceof Error ? err.message : String(err)}), ` +
|
|
153
|
+
"so it cannot sign the job the agent's app host helper would run",
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (!key) {
|
|
157
|
+
throw new AppJobSigningError(
|
|
158
|
+
"The relay has no signing key, so it cannot sign the job the agent's app host helper would run",
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return key;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* An app job: install (`spec`), activate (`version`, `package`) or remove
|
|
166
|
+
* (`purge`, `package`) the plugin's agent half through the agent's root
|
|
167
|
+
* helper — signed with the relay's key for the agent and the job's
|
|
168
|
+
* deadline. Throws {@link AppJobSigningError} (nothing is created) when the
|
|
169
|
+
* key is unusable, a TypeError for parameters that are not the kind's.
|
|
170
|
+
*
|
|
171
|
+
* @param {{ agentLabel: string, plugin: string, kind: 'app-install' | 'app-activate' | 'app-remove', spec?: object, version?: string, purge?: boolean, package?: string }} job
|
|
172
|
+
* @returns {Promise<string>} the job id
|
|
173
|
+
*/
|
|
174
|
+
export async function createAppJob({ agentLabel, plugin, kind, ...params }) {
|
|
175
|
+
if (!Object.hasOwn(AGENT_APP_JOB_KINDS, kind)) {
|
|
176
|
+
throw new TypeError(`Unknown app job kind "${kind}"`);
|
|
177
|
+
}
|
|
178
|
+
const payload = appJobPayload(kind, params);
|
|
179
|
+
const key = await appJobSigningKey();
|
|
180
|
+
return insertJob({
|
|
181
|
+
agentLabel,
|
|
182
|
+
plugin,
|
|
183
|
+
kind,
|
|
184
|
+
stage: null,
|
|
185
|
+
input: null,
|
|
186
|
+
payload: JSON.stringify(payload),
|
|
187
|
+
timeoutMs: AGENT_APP_JOB_KINDS[kind],
|
|
188
|
+
sign: ({ id, issuedAt, expiresAt }) =>
|
|
189
|
+
signAppJob(
|
|
190
|
+
{ agent: agentLabel, route: plugin, jobId: id, kind, payload, issuedAt, expiresAt },
|
|
191
|
+
key,
|
|
192
|
+
),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Finish the pending jobs `rows` as `status` (expired, cancelled) and wake their waiters. */
|
|
197
|
+
function finishPending(db, rows, status, now) {
|
|
198
|
+
const finish = db.prepare(
|
|
199
|
+
"UPDATE agent_jobs SET status = ?, completed_at = ? WHERE id = ? AND status = 'pending'",
|
|
200
|
+
);
|
|
201
|
+
for (const { id } of rows) {
|
|
202
|
+
finish.run(status, now, id);
|
|
203
|
+
events.emit(id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Expire jobs past their deadline. */
|
|
208
|
+
async function expireOverdue(db) {
|
|
209
|
+
const now = new Date().toISOString();
|
|
210
|
+
const overdue = db
|
|
211
|
+
.prepare("SELECT id FROM agent_jobs WHERE status = 'pending' AND deadline_at <= ?")
|
|
212
|
+
.all(now);
|
|
213
|
+
finishPending(db, overdue, 'expired', now);
|
|
214
|
+
// Finished jobs are kept a day for the record, then dropped.
|
|
215
|
+
const cutoff = new Date(Date.now() - 86_400_000).toISOString();
|
|
216
|
+
db.prepare("DELETE FROM agent_jobs WHERE status != 'pending' AND created_at < ?").run(cutoff);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The agent-config `agentJobs` of agent `label`: pending, not yet overdue.
|
|
221
|
+
* A `hook` job carries `stage` and `input`; an app job its parameters
|
|
222
|
+
* (`spec`; `version` and `package`; `purge` and `package`) and the relay's
|
|
223
|
+
* `signature` over them.
|
|
224
|
+
*/
|
|
225
|
+
export async function pendingAgentJobs(label) {
|
|
226
|
+
const db = await getStateDb();
|
|
227
|
+
await expireOverdue(db);
|
|
228
|
+
return db
|
|
229
|
+
.prepare(
|
|
230
|
+
"SELECT * FROM agent_jobs WHERE agent_label = ? AND status = 'pending' ORDER BY created_at",
|
|
231
|
+
)
|
|
232
|
+
.all(label)
|
|
233
|
+
.map((row) => {
|
|
234
|
+
const job = rowToJob(row);
|
|
235
|
+
const base = { id: job.id, route: job.route, kind: job.kind };
|
|
236
|
+
const timing = { timeoutMs: job.timeoutMs, deadlineAt: job.deadlineAt };
|
|
237
|
+
return job.kind === 'hook'
|
|
238
|
+
? { ...base, stage: job.stage, input: job.input, ...timing }
|
|
239
|
+
: { ...base, ...timing, ...job.payload, signature: job.signature };
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** One job by id, or null. */
|
|
244
|
+
export async function findAgentJob(id) {
|
|
245
|
+
const db = await getStateDb();
|
|
246
|
+
const row = db.prepare('SELECT * FROM agent_jobs WHERE id = ?').get(id);
|
|
247
|
+
return row ? rowToJob(row) : null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The agent reports a job's result. Only the agent the job is for may, and
|
|
252
|
+
* only once. The result must fit the job's kind (core `AgentJobResultSchema`):
|
|
253
|
+
* a hook job's `{ ok: true, decision }`, an app job's
|
|
254
|
+
* `{ ok: true, app: { version, unit } }`, or `{ ok: false, error }` (one
|
|
255
|
+
* line). What is stored is the validated result.
|
|
256
|
+
*
|
|
257
|
+
* @returns {Promise<'recorded' | 'not_found' | 'not_pending' | 'invalid'>}
|
|
258
|
+
*/
|
|
259
|
+
export async function completeAgentJob(label, id, result) {
|
|
260
|
+
const db = await getStateDb();
|
|
261
|
+
const row = db
|
|
262
|
+
.prepare('SELECT * FROM agent_jobs WHERE id = ? AND agent_label = ?')
|
|
263
|
+
.get(id, label);
|
|
264
|
+
if (!row) return 'not_found';
|
|
265
|
+
if (row.status !== 'pending') return 'not_pending';
|
|
266
|
+
const schema = row.kind === 'hook' ? AgentHookJobResultSchema : AgentAppJobResultSchema;
|
|
267
|
+
const parsed = schema.safeParse(result);
|
|
268
|
+
if (!parsed.success) return 'invalid';
|
|
269
|
+
const changes = db
|
|
270
|
+
.prepare(
|
|
271
|
+
"UPDATE agent_jobs SET status = 'done', result = ?, completed_at = ? WHERE id = ? AND status = 'pending'",
|
|
272
|
+
)
|
|
273
|
+
.run(JSON.stringify(parsed.data), new Date().toISOString(), id).changes;
|
|
274
|
+
if (Number(changes) === 0) return 'not_pending';
|
|
275
|
+
events.emit(id);
|
|
276
|
+
return 'recorded';
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Wait for a job to finish. Resolves the job's result — `{ ok: true, decision }`
|
|
281
|
+
* (hook) or `{ ok: true, app }` (app job) — `{ ok: false, error }` (the
|
|
282
|
+
* agent reported a failure), or `{ ok: false, error, expired: true }` when
|
|
283
|
+
* the deadline passed (or the job was cancelled): the outcome is unknown.
|
|
284
|
+
*/
|
|
285
|
+
export async function waitForAgentJob(id) {
|
|
286
|
+
const db = await getStateDb();
|
|
287
|
+
for (;;) {
|
|
288
|
+
const row = db.prepare('SELECT * FROM agent_jobs WHERE id = ?').get(id);
|
|
289
|
+
if (!row) return { ok: false, error: 'The job no longer exists' };
|
|
290
|
+
if (row.status === 'done') {
|
|
291
|
+
const result = JSON.parse(row.result);
|
|
292
|
+
return result && typeof result === 'object'
|
|
293
|
+
? result
|
|
294
|
+
: { ok: false, error: 'Malformed result' };
|
|
295
|
+
}
|
|
296
|
+
if (row.status === 'expired' || row.status === 'cancelled') {
|
|
297
|
+
return {
|
|
298
|
+
ok: false,
|
|
299
|
+
expired: true,
|
|
300
|
+
error:
|
|
301
|
+
row.status === 'expired'
|
|
302
|
+
? 'The agent did not report the job before its deadline (offline, or its sync is not running)'
|
|
303
|
+
: 'The job was cancelled',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const untilDeadline = Date.parse(row.deadline_at) - Date.now();
|
|
307
|
+
if (untilDeadline <= 0) {
|
|
308
|
+
await expireOverdue(db);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
await new Promise((resolve) => {
|
|
312
|
+
const timer = setTimeout(done, Math.min(untilDeadline + 10, 5_000));
|
|
313
|
+
function done() {
|
|
314
|
+
clearTimeout(timer);
|
|
315
|
+
events.off(id, done);
|
|
316
|
+
resolve();
|
|
317
|
+
}
|
|
318
|
+
events.once(id, done);
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Cancel a plugin's pending jobs (its operation ended). */
|
|
324
|
+
export async function cancelAgentJobs(plugin) {
|
|
325
|
+
const db = await getStateDb();
|
|
326
|
+
const rows = db
|
|
327
|
+
.prepare("SELECT id FROM agent_jobs WHERE plugin = ? AND status = 'pending'")
|
|
328
|
+
.all(plugin);
|
|
329
|
+
finishPending(db, rows, 'cancelled', new Date().toISOString());
|
|
330
|
+
}
|
|
@@ -12,3 +12,13 @@ export class AppError extends Error {
|
|
|
12
12
|
this.isOperational = true;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A plain Error carrying the HTTP `statusCode` a route answers with, and
|
|
18
|
+
* whatever `extra` names beyond the message (a refusal's `code`,
|
|
19
|
+
* `reasons`, …). Not an {@link AppError}: the error middleware treats those
|
|
20
|
+
* as operational.
|
|
21
|
+
*/
|
|
22
|
+
export function httpError(message, statusCode, extra = {}) {
|
|
23
|
+
return Object.assign(new Error(message), { statusCode, ...extra });
|
|
24
|
+
}
|