@pasko70/pibo 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/apps/chat/chat-request-normalizers.js +7 -0
  2. package/dist/apps/chat/data/chat-data-mappers.js +2 -0
  3. package/dist/apps/chat/loop-api.js +11 -6
  4. package/dist/apps/chat/web-app.js +30 -10
  5. package/dist/apps/chat-ui/assets/{dist-wE9nop9V.js → dist-0E9FVJ6k.js} +1 -1
  6. package/dist/apps/chat-ui/assets/{dist-DlATLa-U.js → dist-BvqC0hRM.js} +1 -1
  7. package/dist/apps/chat-ui/assets/{dist-DUlaXAk7.js → dist-C2BRHNXr.js} +1 -1
  8. package/dist/apps/chat-ui/assets/{dist-nOLTkZrJ.js → dist-C57jNmjf.js} +1 -1
  9. package/dist/apps/chat-ui/assets/{dist-Y-AA2omI.js → dist-CBL8UXjd.js} +1 -1
  10. package/dist/apps/chat-ui/assets/{dist-D-cxLQO1.js → dist-CI-LPHjw.js} +1 -1
  11. package/dist/apps/chat-ui/assets/{dist-LHRs1Nhr.js → dist-CMRFkfl5.js} +1 -1
  12. package/dist/apps/chat-ui/assets/{dist-CS7wdk0Z.js → dist-CpZPhD2y.js} +1 -1
  13. package/dist/apps/chat-ui/assets/{dist-BwKObYnX.js → dist-CuCZ-3_K.js} +1 -1
  14. package/dist/apps/chat-ui/assets/{dist-GdEM8UW1.js → dist-CzC5kPWJ.js} +1 -1
  15. package/dist/apps/chat-ui/assets/{dist-CKtT8YGm.js → dist-D9hLnn1T.js} +1 -1
  16. package/dist/apps/chat-ui/assets/index-BEvjPUor.js +173 -0
  17. package/dist/apps/chat-ui/assets/{index-C0x9nEcf.css → index-DNeE4HrG.css} +1 -1
  18. package/dist/apps/chat-ui/index.html +2 -2
  19. package/dist/apps/chat-vscode-web/assets/index-BLZRi4Ka.js +41 -0
  20. package/dist/apps/chat-vscode-web/assets/index-Bf2JvJ9z.css +2 -0
  21. package/dist/apps/chat-vscode-web/index.html +2 -2
  22. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  23. package/dist/apps/vscode-artifacts/pibo-vscode-ext-1.11.0.vsix +0 -0
  24. package/dist/auth/better-auth.js +49 -1
  25. package/dist/auth/cli.js +220 -0
  26. package/dist/auth/machine-keys.js +258 -0
  27. package/dist/auth/machine-session.js +123 -0
  28. package/dist/bin/pibo.js +0 -0
  29. package/dist/bin/rg.js +0 -0
  30. package/dist/cli-session/localSessionSource.js +34 -12
  31. package/dist/cli.js +6 -0
  32. package/dist/compute/cli.js +7 -0
  33. package/dist/compute/resource-health.js +61 -4
  34. package/dist/config/config.js +5 -0
  35. package/dist/core/events.js +6 -1
  36. package/dist/core/routed-session.js +67 -4
  37. package/dist/core/session-router.js +40 -6
  38. package/dist/data/ingest-service.js +3 -1
  39. package/dist/data/schema.js +4 -0
  40. package/dist/debug/index.js +4 -0
  41. package/dist/debug/trace-status.js +25 -0
  42. package/dist/debug/trace.js +51 -17
  43. package/dist/index.js +1 -0
  44. package/dist/loops/accounting.js +19 -0
  45. package/dist/loops/cli.js +22 -6
  46. package/dist/loops/prompts.js +4 -1
  47. package/dist/loops/service.js +97 -8
  48. package/dist/loops/store.js +128 -17
  49. package/dist/loops/tools.js +24 -7
  50. package/dist/reliability/store.js +34 -7
  51. package/dist/resources/cli.js +15 -0
  52. package/dist/resources/lifecycle.js +28 -4
  53. package/dist/resources/reaper.js +1 -0
  54. package/dist/runs/lifecycle.js +59 -0
  55. package/dist/runs/registry.js +47 -1
  56. package/dist/runs/tools.js +31 -13
  57. package/dist/session-ui/terminalRows.js +66 -2
  58. package/dist/shared/trace-async-agent-runs.js +4 -4
  59. package/dist/shared/trace-event-projection.js +59 -19
  60. package/dist/shared/trace-nodes.js +5 -0
  61. package/dist/shared/trace-page-merge.js +15 -0
  62. package/dist/shared/trace-run-notifications.js +3 -1
  63. package/dist/signals/projector.js +16 -3
  64. package/dist/tools/browser-pool.js +50 -0
  65. package/dist/tools/browser-use-leases.js +12 -8
  66. package/dist/tools/guides.js +8 -1
  67. package/dist/tools/index.js +1 -0
  68. package/package.json +2 -1
  69. package/skills/builtin/loop/SKILL.md +12 -3
  70. package/dist/apps/chat-ui/assets/index-8W_yMHQI.js +0 -173
  71. package/dist/apps/chat-vscode-web/assets/index-B5QK07zO.css +0 -2
  72. package/dist/apps/chat-vscode-web/assets/index-BAMxIaI_.js +0 -41
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-emerald-500:oklch(69.6% .17 162.48);--color-cyan-400:oklch(78.9% .154 211.53);--color-rose-200:oklch(89.2% .058 10.001);--color-rose-300:oklch(81% .117 11.638);--color-rose-500:oklch(64.5% .246 16.439);--color-rose-700:oklch(51.4% .222 16.935);--color-rose-900:oklch(41% .159 10.272);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-700:oklch(37.2% .044 257.287);--color-slate-800:oklch(27.9% .041 260.031);--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-semibold:600;--tracking-wider:.05em;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}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;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);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}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.absolute{position:absolute}.start{inset-inline-start:var(--spacing)}.z-10{z-index:10}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-12{margin-top:calc(var(--spacing) * 12)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.-ml-20{margin-left:calc(var(--spacing) * -20)}.ml-3{margin-left:calc(var(--spacing) * 3)}.block{display:block}.flex{display:flex}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-7{height:calc(var(--spacing) * 7)}.h-full{height:100%}.min-h-0{min-height:calc(var(--spacing) * 0)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-7{width:calc(var(--spacing) * 7)}.w-full{width:100%}.max-w-\[180px\]{max-width:180px}.min-w-0{min-width:calc(var(--spacing) * 0)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.gap-1{gap:calc(var(--spacing) * 1)}.gap-2{gap:calc(var(--spacing) * 2)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-x-auto{overflow-x:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.border{border-style:var(--tw-border-style);border-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-\[\#11a4d4\]{border-color:#11a4d4}.border-rose-700{border-color:var(--color-rose-700)}.border-slate-700{border-color:var(--color-slate-700)}.border-slate-800{border-color:var(--color-slate-800)}.bg-\[\#0a1418\]{background-color:#0a1418}.bg-\[\#0c1820\]{background-color:#0c1820}.bg-\[\#102834\]{background-color:#102834}.bg-cyan-400{background-color:var(--color-cyan-400)}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-rose-500{background-color:var(--color-rose-500)}.bg-rose-900\/40{background-color:#8b083666}@supports (color:color-mix(in lab, red, red)){.bg-rose-900\/40{background-color:color-mix(in oklab, var(--color-rose-900) 40%, transparent)}}.bg-slate-500{background-color:var(--color-slate-500)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[10px\]{font-size:10px}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.text-rose-200{color:var(--color-rose-200)}.text-rose-300{color:var(--color-rose-300)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (hover:hover){.hover\:border-\[\#11a4d4\]:hover{border-color:#11a4d4}.hover\:border-slate-500:hover{border-color:var(--color-slate-500)}.hover\:bg-\[\#102834\]:hover{background-color:#102834}.hover\:bg-rose-900\/30:hover{background-color:#8b08364d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-rose-900\/30:hover{background-color:color-mix(in oklab, var(--color-rose-900) 30%, transparent)}}.hover\:text-\[\#11a4d4\]:hover{color:#11a4d4}.hover\:text-slate-200:hover{color:var(--color-slate-200)}}}:root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;scrollbar-color:#334155 #101d22}html,body,#root{height:100%}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:#101d22;margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Helvetica Neue,sans-serif}code,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.session-signal{display:inline-block;box-shadow:0 0 #64748b00}.session-signal-idle{background:#475569}.session-signal-running{background:#0bda57;animation:1.9s ease-in-out infinite session-signal-slow-pulse}.session-signal-unread{background:#38bdf8;animation:.85s ease-in-out infinite session-signal-fast-pulse}.session-signal-error{background:#ef4444;animation:.55s ease-in-out infinite session-signal-error-blink}@keyframes session-signal-slow-pulse{0%,to{transform:scale(.95);box-shadow:0 0 #0bda5738}50%{transform:scale(1.15);box-shadow:0 0 0 5px #0bda5700}}@keyframes session-signal-fast-pulse{0%,to{transform:scale(.9);box-shadow:0 0 #38bdf861}50%{transform:scale(1.25);box-shadow:0 0 0 6px #38bdf800}}@keyframes session-signal-error-blink{0%,to{opacity:1;transform:scale(1.15);box-shadow:0 0 #ef444473}50%{opacity:.45;transform:scale(.9);box-shadow:0 0 0 6px #ef444400}}.compact-terminal-working-label{color:#737373;vertical-align:baseline;width:10ch;font-size:.92em;display:inline-block}.model-response-markdown{color:#dbe7e8;overflow-wrap:anywhere}.model-response-markdown>:first-child{margin-top:0}.model-response-markdown>:last-child{margin-bottom:0}.model-response-markdown p{margin:0 0 .85rem}.model-response-markdown h1,.model-response-markdown h2,.model-response-markdown h3,.model-response-markdown h4{color:#f8fafc;letter-spacing:0;margin:1.1rem 0 .45rem;font-weight:700;line-height:1.25}.model-response-markdown h1{font-size:1.15rem}.model-response-markdown h2{font-size:1.05rem}.model-response-markdown h3,.model-response-markdown h4{font-size:.95rem}.model-response-markdown a{color:#42c7ec;text-underline-offset:3px;-webkit-text-decoration:underline #42c7ec73;text-decoration:underline #42c7ec73}.model-response-markdown a:hover{color:#7dd3fc;text-decoration-color:currentColor}.model-response-markdown strong{color:#f8fafc;font-weight:700}.model-response-markdown del{color:#94a3b8}.model-response-markdown ul,.model-response-markdown ol{margin:0 0 .95rem;padding-left:1.25rem;list-style-position:outside}.model-response-markdown ul{list-style-type:disc}.model-response-markdown ol{list-style-type:decimal}.model-response-markdown ul ul,.model-response-markdown ol ul{margin:.25rem 0;list-style-type:circle}.model-response-markdown ul ol,.model-response-markdown ol ol{margin:.25rem 0;list-style-type:lower-alpha}.model-response-markdown li{margin:.25rem 0;padding-left:.15rem}.model-response-markdown li::marker{color:#11a4d4}.model-response-markdown blockquote{color:#cbd5e1;background:#0bda5712;border-left:2px solid #0bda57;margin:.95rem 0;padding:.55rem .85rem}.model-response-markdown hr{border:0;border-top:1px solid #334155cc;margin:1rem 0}.model-response-markdown table{border-collapse:collapse;border:1px solid #334155e6;border-radius:3px;width:max-content;max-width:100%;margin:1rem 0;font-size:.82rem;display:block;overflow-x:auto}.model-response-markdown th,.model-response-markdown td{text-align:left;vertical-align:top;border:1px solid #334155cc;padding:.45rem .6rem}.model-response-markdown th{color:#e2f6fb;background:#11a4d41f;font-weight:700}.model-response-markdown td{color:#dbe7e8;background:#0e11166b}.model-response-markdown :not(pre)>code{color:#11a4d4;background:#111a1f;border:1px solid #e2e8f02e;border-radius:3px;padding:.1rem .3rem;font-size:.86em}.model-response-markdown pre{color:#d1d5db;background:#0e1116;border:1px solid #334155e6;border-left:2px solid #0bda57;border-radius:2px;margin:.95rem 0;padding:.85rem;font-size:.78rem;line-height:1.55;overflow-x:auto}.model-response-markdown pre code{color:inherit;font-size:inherit;background:0 0}.compact-terminal-markdown{color:#d4d4d4;overflow-wrap:anywhere;word-break:break-word;font-size:12px;line-height:1.45}.compact-terminal-markdown>:first-child{margin-top:0}.compact-terminal-markdown>:last-child{margin-bottom:0}.compact-terminal-markdown p{margin:0 0 .55rem}.compact-terminal-reasoning{color:#c7b27a}.compact-terminal-reasoning p{margin-bottom:.35rem}.compact-terminal-reasoning li::marker{color:#f59e0b}.compact-terminal-markdown h1,.compact-terminal-markdown h2,.compact-terminal-markdown h3,.compact-terminal-markdown h4{color:#f3f4f6;margin:.8rem 0 .35rem;font-size:.95rem;line-height:1.3}.compact-terminal-markdown a{color:#38bdf8}.compact-terminal-markdown ul,.compact-terminal-markdown ol{margin:0 0 .55rem;padding-left:1.35rem;list-style-position:outside}.compact-terminal-markdown ul{list-style-type:disc}.compact-terminal-markdown ol{list-style-type:decimal}.compact-terminal-markdown ul ul,.compact-terminal-markdown ol ul{margin:.2rem 0;list-style-type:circle}.compact-terminal-markdown ul ol,.compact-terminal-markdown ol ol{margin:.2rem 0;list-style-type:lower-alpha}.compact-terminal-markdown li{margin:.15rem 0;padding-left:.1rem}.compact-terminal-markdown li::marker{color:#38bdf8}.compact-terminal-markdown li>p{margin:.15rem 0}.compact-terminal-markdown input[type=checkbox]{vertical-align:-.12em;accent-color:#38bdf8;margin:0 .45rem 0 -1.15rem}.compact-terminal-markdown blockquote{color:#d4d4d4;background:#38bdf814;border-left:1px solid #38bdf8;margin:.6rem 0;padding:.45rem .7rem}.compact-terminal-markdown :not(pre)>code{color:#38bdf8;background:#161616;border:1px solid #5a5a5a99;border-radius:2px;padding:.05rem .25rem}.compact-terminal-markdown pre{color:#d4d4d4;background:#111;border:1px solid #2a2a2a;max-width:100%;margin:.65rem 0;padding:.65rem .8rem;font-size:.78rem;line-height:1.5;overflow-x:auto}.compact-terminal-markdown table{border-collapse:collapse;max-width:100%;display:block;overflow-x:auto}.compact-terminal-markdown th,.compact-terminal-markdown td{vertical-align:top;border:1px solid #2a2a2a;padding:.25rem .4rem}.compact-terminal-markdown th{color:#f3f4f6;background:#161616}.compact-terminal-markdown .token.comment,.model-response-markdown .token.comment,.compact-terminal-markdown .token.prolog,.model-response-markdown .token.prolog,.compact-terminal-markdown .token.doctype,.model-response-markdown .token.doctype,.compact-terminal-markdown .token.cdata,.model-response-markdown .token.cdata{color:#737373}.compact-terminal-markdown .token.punctuation,.model-response-markdown .token.punctuation{color:#94a3b8}.compact-terminal-markdown .token.property,.model-response-markdown .token.property,.compact-terminal-markdown .token.tag,.model-response-markdown .token.tag,.compact-terminal-markdown .token.boolean,.model-response-markdown .token.boolean,.compact-terminal-markdown .token.number,.model-response-markdown .token.number,.compact-terminal-markdown .token.constant,.model-response-markdown .token.constant,.compact-terminal-markdown .token.symbol,.model-response-markdown .token.symbol{color:#60a5fa}.compact-terminal-markdown .token.selector,.model-response-markdown .token.selector,.compact-terminal-markdown .token.attr-name,.model-response-markdown .token.attr-name,.compact-terminal-markdown .token.string,.model-response-markdown .token.string,.compact-terminal-markdown .token.char,.model-response-markdown .token.char,.compact-terminal-markdown .token.builtin,.model-response-markdown .token.builtin{color:#22c55e}.compact-terminal-markdown .token.operator,.model-response-markdown .token.operator,.compact-terminal-markdown .token.entity,.model-response-markdown .token.entity,.compact-terminal-markdown .token.url,.model-response-markdown .token.url,.compact-terminal-markdown .token.variable,.model-response-markdown .token.variable{color:#facc15}.compact-terminal-markdown .token.atrule,.model-response-markdown .token.atrule,.compact-terminal-markdown .token.attr-value,.model-response-markdown .token.attr-value,.compact-terminal-markdown .token.function,.model-response-markdown .token.function,.compact-terminal-markdown .token.class-name,.model-response-markdown .token.class-name{color:#38bdf8}.compact-terminal-markdown .token.keyword,.model-response-markdown .token.keyword{color:#d946ef}.compact-terminal-markdown .token.regex,.model-response-markdown .token.regex,.compact-terminal-markdown .token.important,.model-response-markdown .token.important{color:#fb923c}.compact-terminal-markdown .token.deleted,.model-response-markdown .token.deleted{color:#ef4444}.compact-terminal-markdown .token.inserted,.model-response-markdown .token.inserted{color:#22c55e}.compact-terminal-json{--w-rjv-background-color:transparent;--w-rjv-border-left:#2a2a2a;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.compact-terminal-function-call{overflow-wrap:anywhere;word-break:break-word}.compact-terminal-json-toggle{font:inherit;cursor:pointer;background:0 0;border:0;margin:0;padding:0}.compact-terminal-json-toggle:hover{text-underline-offset:2px;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.compact-terminal-json-caret{color:#a3a3a3;width:.75em;text-decoration:none;display:inline-block}.context-files-view .mdxeditor{--baseBg:#151f24;--baseBorderColor:#334155;--baseTextColor:#dbe7e8;--accentBg:#11a4d42e;--accentTextColor:#f8fafc;--accentBaseColor:#11a4d4;--basePageBg:transparent;color:#dbe7e8;flex-direction:column;min-height:100%;display:flex}.context-files-view .mdxeditor-toolbar{z-index:2;background:#151f24;border-bottom:1px solid #1e293b;position:sticky;top:0;overflow-x:auto}.context-files-view .mdxeditor-toolbar [role=toolbar]{gap:.3rem;padding:.45rem .55rem}.context-files-view .mdxeditor-root-contenteditable,.context-files-mdx-content{color:#dbe7e8;overflow-wrap:anywhere;word-break:break-word;width:100%;min-width:0;min-height:calc(100vh - 14rem);padding:1.15rem 1.25rem 5rem}.context-files-mdx-content h1,.context-files-mdx-content h2,.context-files-mdx-content h3,.context-files-mdx-content h4,.context-files-mdx-content h5,.context-files-mdx-content h6{color:#f8fafc;margin:1.2rem 0 .55rem;line-height:1.2}.context-files-mdx-content h1{margin-top:0;font-size:1.6rem}.context-files-mdx-content h2{font-size:1.25rem}.context-files-mdx-content h3{font-size:1.05rem}.context-files-mdx-content ul,.context-files-mdx-content ol{margin:0 0 1rem;padding-left:1.4rem}.context-files-mdx-content blockquote{color:#cbd5e1;border-left:4px solid #11a4d473;margin-left:0;padding-left:.9rem}.context-files-mdx-content table{border-collapse:collapse;width:100%;display:block;overflow-x:auto}.context-files-mdx-content th,.context-files-mdx-content td{border:1px solid #334155;padding:.5rem .6rem}.context-files-view .cm-editor,.context-files-view .cm-gutters{color:#dbe7e8;background:#0e1116}.context-files-view .cm-scroller,.context-files-view pre{max-width:100%;overflow-x:auto}.context-files-view .cm-cursor,.context-files-view .cm-dropCursor{border-left-color:#11a4d4}.context-files-view [class*=_iconButton_],.context-files-view [class*=_toolbarButton_],.context-files-view [class*=_toolbarToggleItem_],.context-files-view [class*=_selectTrigger_],.context-files-view [class*=_toolbarNodeKindSelectTrigger_],.context-files-view [class*=_toolbarButtonSelectTrigger_]{color:#94a3b8!important;box-shadow:none!important;background:0 0!important;border-color:#2a3943!important;font-weight:400!important}.context-files-view [class*=_iconButton_] svg,.context-files-view [class*=_toolbarButton_] svg,.context-files-view [class*=_toolbarToggleItem_] svg,.context-files-view [class*=_selectTrigger_] svg,.context-files-view [class*=_toolbarNodeKindSelectTrigger_] svg,.context-files-view [class*=_toolbarButtonSelectTrigger_] svg{opacity:.88;color:#94a3b8!important;stroke:currentColor!important;width:18px!important;height:18px!important}.context-files-view [class*=_iconButton_]:hover,.context-files-view [class*=_toolbarButton_]:hover,.context-files-view [class*=_toolbarToggleItem_]:hover,.context-files-view [class*=_selectTrigger_]:hover,.context-files-view [class*=_toolbarNodeKindSelectTrigger_]:hover,.context-files-view [class*=_toolbarButtonSelectTrigger_]:hover{color:#cbd5e1!important;background:#ffffff08!important;border-color:#334155!important}.context-files-view [class*=_iconButton_]:hover svg,.context-files-view [class*=_toolbarButton_]:hover svg,.context-files-view [class*=_toolbarToggleItem_]:hover svg,.context-files-view [class*=_selectTrigger_]:hover svg,.context-files-view [class*=_toolbarNodeKindSelectTrigger_]:hover svg,.context-files-view [class*=_toolbarButtonSelectTrigger_]:hover svg{opacity:.94;color:#cbd5e1!important}.context-files-view [data-state=on],.context-files-view [aria-checked=true]{color:#dbe7e8!important;background:#11a4d41f!important;border-color:#11a4d473!important}.context-files-view [data-state=on] svg,.context-files-view [aria-checked=true] svg{opacity:.94;color:#cbd5e1!important}.context-files-view [class*=_selectContent_],.context-files-view [class*=_dropdownMenuContent_],.context-files-view [class*=_dialogContent_]{color:#dbe7e8!important;background:#111a1f!important;border:1px solid #334155!important}.context-files-view [class*=_toolbarToggleSingleGroup_]{gap:.3rem}.context-files-plain-fallback{flex-direction:column;height:100%;min-height:0;display:flex}.context-files-plain-fallback__notice{color:#fde68a;background:#f59e0b1a;border-bottom:1px solid #1e293b;margin:0;padding:.75rem 1rem;font-size:.84rem}.context-files-plain-fallback__textarea{color:#dbe7e8;min-height:0;font:inherit;resize:none;background:#151f24;border:0;outline:none;flex:1;padding:1rem 1.15rem 4rem;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;line-height:1.55;overflow:auto}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="theme-color" content="#101d22" />
7
7
  <title>Pibo</title>
8
- <script type="module" crossorigin src="/apps/chat-vscode/assets/index-BAMxIaI_.js"></script>
9
- <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-B5QK07zO.css">
8
+ <script type="module" crossorigin src="/apps/chat-vscode/assets/index-BLZRi4Ka.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/apps/chat-vscode/assets/index-Bf2JvJ9z.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
@@ -6,6 +6,8 @@ import { getMigrations } from "better-auth/db/migration";
6
6
  import { bearer } from "better-auth/plugins";
7
7
  import { loadPiboConfig } from "../config/config.js";
8
8
  import { piboHomePath } from "../core/pibo-home.js";
9
+ import { createMachineKeyAuthenticator } from "./machine-keys.js";
10
+ import { createMachineSessionManager } from "./machine-session.js";
9
11
  import { createForbiddenAuthError, createUnauthenticatedError } from "./types.js";
10
12
  const SESSION_EXPIRES_IN_SECONDS = 60 * 60 * 24 * 90;
11
13
  const SESSION_UPDATE_AGE_SECONDS = 60 * 60;
@@ -64,6 +66,8 @@ export function createBetterAuthService(options = {}) {
64
66
  const googleClientSecret = requiredOption(options.googleClientSecret ?? authConfig?.googleClientSecret, "auth.googleClientSecret");
65
67
  const secret = requiredSecret(options.secret ?? authConfig?.secret);
66
68
  const allowedEmails = requiredAllowedEmails(options, authConfig?.allowedEmails);
69
+ const machineKeys = createMachineKeyAuthenticator(options.machineKeyStorePath ?? authConfig?.machineKeyStorePath);
70
+ const machineSessions = createMachineSessionManager({ secret, machineKeys });
67
71
  const database = createDatabase(options.databasePath ?? authConfig?.databasePath ?? piboHomePath("auth.sqlite"));
68
72
  const trustedOrigins = options.trustedOrigins ?? authConfig?.trustedOrigins;
69
73
  const authOptions = {
@@ -86,6 +90,12 @@ export function createBetterAuthService(options = {}) {
86
90
  plugins: [bearer()],
87
91
  };
88
92
  const auth = betterAuth(authOptions);
93
+ const requireAllowedMachineSession = (session) => {
94
+ const email = session.identity.email?.toLowerCase();
95
+ if (!email || !allowedEmails.has(email))
96
+ throw createForbiddenAuthError();
97
+ return session;
98
+ };
89
99
  return {
90
100
  name: "better-auth",
91
101
  async start() {
@@ -96,6 +106,9 @@ export function createBetterAuthService(options = {}) {
96
106
  database.close();
97
107
  },
98
108
  async getSession(headers) {
109
+ const machineSession = machineKeys.getSession(headers) ?? machineSessions.getSession(headers);
110
+ if (machineSession)
111
+ return requireAllowedMachineSession(machineSession);
99
112
  const session = await auth.api.getSession({ headers });
100
113
  if (!session)
101
114
  return undefined;
@@ -123,7 +136,42 @@ export function createBetterAuthService(options = {}) {
123
136
  throw createUnauthenticatedError();
124
137
  return session;
125
138
  },
126
- handleRequest(request) {
139
+ async handleRequest(request) {
140
+ const url = new URL(request.url);
141
+ if (url.pathname === "/api/auth/machine-session") {
142
+ if (request.method === "DELETE") {
143
+ return new Response(null, {
144
+ status: 204,
145
+ headers: { "set-cookie": machineSessions.clearHeader(), "cache-control": "no-store" },
146
+ });
147
+ }
148
+ if (request.method !== "POST") {
149
+ return new Response(JSON.stringify({ error: "Method not allowed" }), {
150
+ status: 405,
151
+ headers: { "content-type": "application/json", allow: "POST, DELETE", "cache-control": "no-store" },
152
+ });
153
+ }
154
+ const loopbackHosts = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
155
+ if (url.protocol !== "https:" && !loopbackHosts.has(url.hostname)) {
156
+ return new Response(JSON.stringify({ error: "Machine session exchange requires HTTPS outside loopback" }), {
157
+ status: 400,
158
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
159
+ });
160
+ }
161
+ const authentication = machineKeys.authenticate(request.headers);
162
+ if (!authentication)
163
+ throw createUnauthenticatedError();
164
+ const session = requireAllowedMachineSession(authentication.session);
165
+ const created = machineSessions.create({ ...authentication, session });
166
+ return new Response(JSON.stringify({ identity: created.session.identity, expiresAt: created.expiresAt.toISOString() }), {
167
+ status: 200,
168
+ headers: {
169
+ "content-type": "application/json",
170
+ "cache-control": "no-store",
171
+ "set-cookie": created.header,
172
+ },
173
+ });
174
+ }
127
175
  return auth.handler(request);
128
176
  },
129
177
  };
@@ -0,0 +1,220 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { Command } from "commander";
5
+ import { loadPiboConfig } from "../config/config.js";
6
+ import { piboHomePath } from "../core/pibo-home.js";
7
+ import { generateMachineKey, getDefaultMachineKeyStorePath, importMachineKeyRecord, listMachineKeys, parseMachineKeyRecord, revokeMachineKey, } from "./machine-keys.js";
8
+ function printJson(value) {
9
+ console.log(JSON.stringify(value, null, 2));
10
+ }
11
+ function printAuthDiscovery() {
12
+ console.log(`pibo auth - manage Web authentication
13
+
14
+ Commands:
15
+ machine-key Manage revocable machine identities
16
+
17
+ Next:
18
+ pibo auth machine-key
19
+ `);
20
+ }
21
+ function printMachineKeyDiscovery() {
22
+ console.log(`pibo auth machine-key - manage machine identities
23
+
24
+ Commands:
25
+ identity Resolve one existing user from the allowed email set
26
+ generate Generate local secret and hash-only record files
27
+ import Import one hash-only record into the server store
28
+ list List keys without hashes or secrets
29
+ revoke Revoke one key by id
30
+
31
+ Next:
32
+ pibo auth machine-key <command> --help
33
+ `);
34
+ }
35
+ function readJsonInput(path) {
36
+ const text = path === "-" ? readFileSync(0, "utf8") : readFileSync(resolve(path), "utf8");
37
+ return JSON.parse(text);
38
+ }
39
+ function parseIdentity(value) {
40
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
41
+ throw new Error("Identity input must be a JSON object");
42
+ }
43
+ const input = value;
44
+ if (typeof input.userId !== "string" || input.userId.length === 0) {
45
+ throw new Error("Identity input requires userId");
46
+ }
47
+ if (typeof input.email !== "string" || input.email.length === 0) {
48
+ throw new Error("Identity input requires email");
49
+ }
50
+ for (const field of ["name", "image"]) {
51
+ if (input[field] !== undefined && input[field] !== null && typeof input[field] !== "string") {
52
+ throw new Error(`Identity input ${field} must be a string when present`);
53
+ }
54
+ }
55
+ return {
56
+ userId: input.userId,
57
+ email: input.email,
58
+ ...(typeof input.name === "string" && input.name.length > 0 ? { name: input.name } : {}),
59
+ ...(typeof input.image === "string" && input.image.length > 0 ? { image: input.image } : {}),
60
+ };
61
+ }
62
+ function configuredAuthDatabasePath() {
63
+ return resolve(loadPiboConfig().auth?.databasePath ?? piboHomePath("auth.sqlite"));
64
+ }
65
+ function resolveAllowedIdentity(email) {
66
+ const allowedEmails = new Set((loadPiboConfig().auth?.allowedEmails ?? []).map((candidate) => candidate.trim().toLowerCase()).filter(Boolean));
67
+ const normalizedEmail = email?.trim().toLowerCase();
68
+ if (normalizedEmail && !allowedEmails.has(normalizedEmail)) {
69
+ throw new Error(`Email "${email}" is not present in auth.allowedEmails`);
70
+ }
71
+ const database = new DatabaseSync(configuredAuthDatabasePath(), { readOnly: true });
72
+ try {
73
+ const rows = database.prepare('SELECT id, email, name, image FROM "user"').all();
74
+ const matches = rows.filter((row) => typeof row.id === "string" &&
75
+ typeof row.email === "string" &&
76
+ allowedEmails.has(row.email.toLowerCase()) &&
77
+ (normalizedEmail === undefined || row.email.toLowerCase() === normalizedEmail));
78
+ if (matches.length === 0) {
79
+ throw new Error(normalizedEmail
80
+ ? `No Better Auth user exists for allowed email "${email}"`
81
+ : "No Better Auth user matches auth.allowedEmails");
82
+ }
83
+ if (matches.length > 1) {
84
+ throw new Error("Multiple Better Auth users match auth.allowedEmails; pass --email to select one");
85
+ }
86
+ const row = matches[0];
87
+ return {
88
+ userId: row.id,
89
+ email: row.email,
90
+ ...(typeof row.name === "string" && row.name.length > 0 ? { name: row.name } : {}),
91
+ ...(typeof row.image === "string" && row.image.length > 0 ? { image: row.image } : {}),
92
+ };
93
+ }
94
+ finally {
95
+ database.close();
96
+ }
97
+ }
98
+ function requireNewOutputPath(path, label) {
99
+ const resolvedPath = resolve(path);
100
+ if (existsSync(resolvedPath))
101
+ throw new Error(`${label} already exists: ${resolvedPath}`);
102
+ return resolvedPath;
103
+ }
104
+ function writePrivateNewFile(path, content) {
105
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
106
+ writeFileSync(path, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
107
+ }
108
+ function parseExpiresAt(value) {
109
+ if (value === undefined)
110
+ return undefined;
111
+ const timestamp = Date.parse(value);
112
+ if (Number.isNaN(timestamp))
113
+ throw new Error("--expires-at must be an ISO timestamp");
114
+ if (timestamp <= Date.now())
115
+ throw new Error("--expires-at must be in the future");
116
+ return new Date(timestamp).toISOString();
117
+ }
118
+ export async function runAuthCli(argv = process.argv) {
119
+ if (argv.length <= 2 || argv[2] === "--help" || argv[2] === "-h") {
120
+ printAuthDiscovery();
121
+ return;
122
+ }
123
+ if (argv[2] === "machine-key" && (argv.length <= 3 || argv[3] === "--help" || argv[3] === "-h")) {
124
+ printMachineKeyDiscovery();
125
+ return;
126
+ }
127
+ const program = new Command();
128
+ program.name("pibo auth").description("Manage Pibo Web authentication");
129
+ const machineKey = program.command("machine-key").description("Manage revocable machine identities");
130
+ machineKey
131
+ .command("identity")
132
+ .description("Resolve an existing allowed Better Auth user")
133
+ .option("--email <email>", "Select one allowed Google account email when multiple users match")
134
+ .option("--json", "Print JSON")
135
+ .action((options) => {
136
+ const identity = resolveAllowedIdentity(options.email);
137
+ if (options.json)
138
+ printJson(identity);
139
+ else
140
+ console.log(`${identity.userId}\t${identity.email}\t${identity.name ?? ""}`);
141
+ });
142
+ machineKey
143
+ .command("generate")
144
+ .description("Generate a local raw secret and hash-only import record")
145
+ .requiredOption("--identity-file <path>", "Identity JSON from the identity command")
146
+ .requiredOption("--label <label>", "Operator label for this machine")
147
+ .requiredOption("--secret-output <path>", "New root-only raw-secret file")
148
+ .requiredOption("--record-output <path>", "New root-only hash-record file")
149
+ .option("--expires-at <timestamp>", "Optional future ISO expiration timestamp")
150
+ .action((options) => {
151
+ const secretOutput = requireNewOutputPath(options.secretOutput, "Secret output");
152
+ const recordOutput = requireNewOutputPath(options.recordOutput, "Record output");
153
+ if (secretOutput === recordOutput)
154
+ throw new Error("Secret and record output paths must differ");
155
+ const identity = parseIdentity(readJsonInput(options.identityFile));
156
+ const generated = generateMachineKey({
157
+ label: options.label,
158
+ identity,
159
+ expiresAt: parseExpiresAt(options.expiresAt),
160
+ });
161
+ try {
162
+ writePrivateNewFile(secretOutput, `${generated.token}\n`);
163
+ writePrivateNewFile(recordOutput, `${JSON.stringify(generated.record, null, 2)}\n`);
164
+ }
165
+ catch (error) {
166
+ if (existsSync(secretOutput))
167
+ unlinkSync(secretOutput);
168
+ if (existsSync(recordOutput))
169
+ unlinkSync(recordOutput);
170
+ throw error;
171
+ }
172
+ printJson({
173
+ id: generated.record.id,
174
+ label: generated.record.label,
175
+ email: generated.record.identity.email,
176
+ expiresAt: generated.record.expiresAt,
177
+ secretOutput,
178
+ recordOutput,
179
+ });
180
+ });
181
+ machineKey
182
+ .command("import")
183
+ .description("Import one hash-only machine-key record")
184
+ .requiredOption("--file <path>", "Record JSON path, or - for stdin")
185
+ .option("--store <path>", "Alternate machine-key store path")
186
+ .action((options) => {
187
+ const parsed = parseMachineKeyRecord(readJsonInput(options.file));
188
+ const existingIdentity = resolveAllowedIdentity(parsed.identity.email);
189
+ if (existingIdentity.userId !== parsed.identity.userId) {
190
+ throw new Error(`Machine identity userId does not match the Better Auth user for "${parsed.identity.email}"`);
191
+ }
192
+ const imported = importMachineKeyRecord(parsed, options.store ?? getDefaultMachineKeyStorePath());
193
+ printJson({ id: imported.id, label: imported.label, email: imported.identity.email, createdAt: imported.createdAt });
194
+ });
195
+ machineKey
196
+ .command("list")
197
+ .description("List machine keys without hashes or secrets")
198
+ .option("--store <path>", "Alternate machine-key store path")
199
+ .option("--json", "Print JSON")
200
+ .action((options) => {
201
+ const keys = listMachineKeys(options.store ?? getDefaultMachineKeyStorePath());
202
+ if (options.json) {
203
+ printJson(keys);
204
+ return;
205
+ }
206
+ for (const key of keys) {
207
+ console.log(`${key.id}\t${key.status}\t${key.identity.email ?? ""}\t${key.label}`);
208
+ }
209
+ });
210
+ machineKey
211
+ .command("revoke")
212
+ .description("Revoke one machine key")
213
+ .argument("<id>", "Machine-key id")
214
+ .option("--store <path>", "Alternate machine-key store path")
215
+ .action((id, options) => {
216
+ const record = revokeMachineKey(id, options.store ?? getDefaultMachineKeyStorePath());
217
+ printJson({ id: record.id, label: record.label, revokedAt: record.revokedAt });
218
+ });
219
+ await program.parseAsync(argv);
220
+ }
@@ -0,0 +1,258 @@
1
+ import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
3
+ import { dirname, resolve } from "node:path";
4
+ import { piboHomePath } from "../core/pibo-home.js";
5
+ export const PIBO_MACHINE_KEY_HEADER = "x-pibo-machine-key";
6
+ export const MACHINE_KEY_STORE_VERSION = 1;
7
+ const MACHINE_KEY_ID_PATTERN = /^[a-f0-9]{16}$/;
8
+ const MACHINE_KEY_HASH_PATTERN = /^[a-f0-9]{64}$/;
9
+ const MACHINE_KEY_TOKEN_PATTERN = /^pibo_mk_([a-f0-9]{16})_([A-Za-z0-9_-]{43})$/;
10
+ const MACHINE_KEY_LABEL_MAX_LENGTH = 120;
11
+ export function getDefaultMachineKeyStorePath() {
12
+ return piboHomePath("machine-keys.json");
13
+ }
14
+ function hashMachineKeyToken(token) {
15
+ return createHash("sha256").update(token, "utf8").digest();
16
+ }
17
+ function parseIsoTimestamp(value, field) {
18
+ if (typeof value !== "string" || value.length === 0 || Number.isNaN(Date.parse(value))) {
19
+ throw new Error(`${field} must be an ISO timestamp`);
20
+ }
21
+ return new Date(value).toISOString();
22
+ }
23
+ function optionalString(value, field) {
24
+ if (value === undefined)
25
+ return undefined;
26
+ if (typeof value !== "string" || value.length === 0)
27
+ throw new Error(`${field} must be a non-empty string`);
28
+ return value;
29
+ }
30
+ function parseIdentity(value) {
31
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
32
+ throw new Error("identity must be an object");
33
+ }
34
+ const input = value;
35
+ const allowedKeys = new Set(["userId", "email", "name", "image", "provider"]);
36
+ for (const key of Object.keys(input)) {
37
+ if (!allowedKeys.has(key))
38
+ throw new Error(`identity contains unsupported field "${key}"`);
39
+ }
40
+ if (typeof input.userId !== "string" || input.userId.length === 0) {
41
+ throw new Error("identity.userId must be a non-empty string");
42
+ }
43
+ if (input.provider !== "machine-key") {
44
+ throw new Error('identity.provider must be "machine-key"');
45
+ }
46
+ const email = optionalString(input.email, "identity.email");
47
+ if (!email)
48
+ throw new Error("identity.email must be a non-empty string");
49
+ return {
50
+ userId: input.userId,
51
+ email,
52
+ name: optionalString(input.name, "identity.name"),
53
+ image: optionalString(input.image, "identity.image"),
54
+ provider: "machine-key",
55
+ };
56
+ }
57
+ export function parseMachineKeyRecord(value) {
58
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
59
+ throw new Error("Machine-key record must be an object");
60
+ }
61
+ const input = value;
62
+ const allowedKeys = new Set(["id", "label", "hash", "identity", "createdAt", "expiresAt", "revokedAt"]);
63
+ for (const key of Object.keys(input)) {
64
+ if (!allowedKeys.has(key))
65
+ throw new Error(`Machine-key record contains unsupported field "${key}"`);
66
+ }
67
+ if (typeof input.id !== "string" || !MACHINE_KEY_ID_PATTERN.test(input.id)) {
68
+ throw new Error("Machine-key record id must be 16 lowercase hexadecimal characters");
69
+ }
70
+ if (typeof input.label !== "string" ||
71
+ input.label.trim().length === 0 ||
72
+ input.label.trim().length > MACHINE_KEY_LABEL_MAX_LENGTH) {
73
+ throw new Error(`Machine-key record label must contain 1-${MACHINE_KEY_LABEL_MAX_LENGTH} characters`);
74
+ }
75
+ if (typeof input.hash !== "string" || !MACHINE_KEY_HASH_PATTERN.test(input.hash)) {
76
+ throw new Error("Machine-key record hash must be 64 lowercase hexadecimal characters");
77
+ }
78
+ const createdAt = parseIsoTimestamp(input.createdAt, "createdAt");
79
+ const expiresAt = input.expiresAt === undefined ? undefined : parseIsoTimestamp(input.expiresAt, "expiresAt");
80
+ const revokedAt = input.revokedAt === undefined ? undefined : parseIsoTimestamp(input.revokedAt, "revokedAt");
81
+ return {
82
+ id: input.id,
83
+ label: input.label.trim(),
84
+ hash: input.hash,
85
+ identity: parseIdentity(input.identity),
86
+ createdAt,
87
+ ...(expiresAt ? { expiresAt } : {}),
88
+ ...(revokedAt ? { revokedAt } : {}),
89
+ };
90
+ }
91
+ function parseMachineKeyStore(value) {
92
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
93
+ throw new Error("Machine-key store must be an object");
94
+ }
95
+ const input = value;
96
+ if (input.version !== MACHINE_KEY_STORE_VERSION) {
97
+ throw new Error(`Machine-key store version must be ${MACHINE_KEY_STORE_VERSION}`);
98
+ }
99
+ if (!Array.isArray(input.keys))
100
+ throw new Error("Machine-key store keys must be an array");
101
+ const keys = input.keys.map(parseMachineKeyRecord);
102
+ const ids = new Set();
103
+ for (const key of keys) {
104
+ if (ids.has(key.id))
105
+ throw new Error(`Duplicate machine-key id "${key.id}"`);
106
+ ids.add(key.id);
107
+ }
108
+ return { version: MACHINE_KEY_STORE_VERSION, keys };
109
+ }
110
+ function assertPrivateStoreFile(path) {
111
+ if (process.platform === "win32")
112
+ return;
113
+ const stat = statSync(path);
114
+ if ((stat.mode & 0o077) !== 0) {
115
+ throw new Error(`Machine-key store must not be accessible by group or other users: ${path}`);
116
+ }
117
+ const currentUid = process.getuid?.();
118
+ if (currentUid !== undefined && stat.uid !== currentUid) {
119
+ throw new Error(`Machine-key store must be owned by the current user: ${path}`);
120
+ }
121
+ }
122
+ export function readMachineKeyStore(path = getDefaultMachineKeyStorePath()) {
123
+ const resolvedPath = resolve(path);
124
+ if (!existsSync(resolvedPath))
125
+ return { version: MACHINE_KEY_STORE_VERSION, keys: [] };
126
+ assertPrivateStoreFile(resolvedPath);
127
+ return parseMachineKeyStore(JSON.parse(readFileSync(resolvedPath, "utf8")));
128
+ }
129
+ function writeMachineKeyStore(store, path) {
130
+ const resolvedPath = resolve(path);
131
+ mkdirSync(dirname(resolvedPath), { recursive: true, mode: 0o700 });
132
+ const temporaryPath = `${resolvedPath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
133
+ try {
134
+ writeFileSync(temporaryPath, `${JSON.stringify(store, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
135
+ renameSync(temporaryPath, resolvedPath);
136
+ chmodSync(resolvedPath, 0o600);
137
+ }
138
+ finally {
139
+ if (existsSync(temporaryPath))
140
+ unlinkSync(temporaryPath);
141
+ }
142
+ }
143
+ export function generateMachineKey(options) {
144
+ const id = randomBytes(8).toString("hex");
145
+ const secret = randomBytes(32).toString("base64url");
146
+ const token = `pibo_mk_${id}_${secret}`;
147
+ const now = options.now ?? new Date();
148
+ const expiresAt = options.expiresAt === undefined
149
+ ? undefined
150
+ : typeof options.expiresAt === "string"
151
+ ? parseIsoTimestamp(options.expiresAt, "expiresAt")
152
+ : options.expiresAt.toISOString();
153
+ const record = parseMachineKeyRecord({
154
+ id,
155
+ label: options.label,
156
+ hash: hashMachineKeyToken(token).toString("hex"),
157
+ identity: { ...options.identity, provider: "machine-key" },
158
+ createdAt: now.toISOString(),
159
+ ...(expiresAt ? { expiresAt } : {}),
160
+ });
161
+ return { token, record };
162
+ }
163
+ export function importMachineKeyRecord(recordValue, path = getDefaultMachineKeyStorePath()) {
164
+ const record = parseMachineKeyRecord(recordValue);
165
+ const store = readMachineKeyStore(path);
166
+ if (store.keys.some((candidate) => candidate.id === record.id)) {
167
+ throw new Error(`Machine-key id "${record.id}" already exists`);
168
+ }
169
+ store.keys.push(record);
170
+ writeMachineKeyStore(store, path);
171
+ return record;
172
+ }
173
+ function machineKeyStatus(record, now) {
174
+ if (record.revokedAt)
175
+ return "revoked";
176
+ if (record.expiresAt && Date.parse(record.expiresAt) <= now.getTime())
177
+ return "expired";
178
+ return "active";
179
+ }
180
+ export function listMachineKeys(path = getDefaultMachineKeyStorePath(), now = new Date()) {
181
+ return readMachineKeyStore(path).keys.map((record) => {
182
+ const { hash: _hash, ...listed } = record;
183
+ return { ...listed, status: machineKeyStatus(record, now) };
184
+ });
185
+ }
186
+ export function revokeMachineKey(id, path = getDefaultMachineKeyStorePath(), now = new Date()) {
187
+ if (!MACHINE_KEY_ID_PATTERN.test(id))
188
+ throw new Error("Machine-key id must be 16 lowercase hexadecimal characters");
189
+ const store = readMachineKeyStore(path);
190
+ const index = store.keys.findIndex((candidate) => candidate.id === id);
191
+ if (index < 0)
192
+ throw new Error(`Unknown machine-key id "${id}"`);
193
+ const current = store.keys[index];
194
+ const revoked = { ...current, revokedAt: current.revokedAt ?? now.toISOString() };
195
+ store.keys[index] = revoked;
196
+ writeMachineKeyStore(store, path);
197
+ return revoked;
198
+ }
199
+ function storeSignature(path) {
200
+ try {
201
+ const stat = statSync(path);
202
+ return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`;
203
+ }
204
+ catch (error) {
205
+ if (error.code === "ENOENT")
206
+ return "missing";
207
+ throw error;
208
+ }
209
+ }
210
+ export function createMachineKeyAuthenticator(path = getDefaultMachineKeyStorePath()) {
211
+ const resolvedPath = resolve(path);
212
+ let cachedSignature;
213
+ let keysById = new Map();
214
+ const refresh = () => {
215
+ const signature = storeSignature(resolvedPath);
216
+ if (signature === cachedSignature)
217
+ return;
218
+ const store = readMachineKeyStore(resolvedPath);
219
+ keysById = new Map(store.keys.map((record) => [record.id, record]));
220
+ cachedSignature = signature;
221
+ };
222
+ const sessionForRecord = (record) => ({
223
+ identity: { ...record.identity },
224
+ sessionId: `machine-key:${record.id}`,
225
+ ...(record.expiresAt ? { expiresAt: new Date(record.expiresAt) } : {}),
226
+ });
227
+ const getSessionById = (id) => {
228
+ refresh();
229
+ const record = keysById.get(id);
230
+ if (!record || machineKeyStatus(record, new Date()) !== "active")
231
+ return undefined;
232
+ return sessionForRecord(record);
233
+ };
234
+ const authenticate = (headers) => {
235
+ const token = headers.get(PIBO_MACHINE_KEY_HEADER)?.trim();
236
+ if (!token)
237
+ return undefined;
238
+ const match = MACHINE_KEY_TOKEN_PATTERN.exec(token);
239
+ if (!match)
240
+ return undefined;
241
+ refresh();
242
+ const record = keysById.get(match[1]);
243
+ if (!record || machineKeyStatus(record, new Date()) !== "active")
244
+ return undefined;
245
+ const expected = Buffer.from(record.hash, "hex");
246
+ const actual = hashMachineKeyToken(token);
247
+ if (expected.length !== actual.length || !timingSafeEqual(expected, actual))
248
+ return undefined;
249
+ return { id: record.id, session: sessionForRecord(record) };
250
+ };
251
+ return {
252
+ authenticate,
253
+ getSession(headers) {
254
+ return authenticate(headers)?.session;
255
+ },
256
+ getSessionById,
257
+ };
258
+ }