agent-relay-server 0.83.0 → 0.84.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 (71) hide show
  1. package/docs/openapi.json +113 -1
  2. package/package.json +2 -2
  3. package/public/assets/{MessageBubble-BmgXwQ3z.js → MessageBubble-BTBQr8uG.js} +2 -2
  4. package/public/assets/{MessageBubble-BmgXwQ3z.js.map → MessageBubble-BTBQr8uG.js.map} +1 -1
  5. package/public/assets/{PlanGraphPanel-FfVHi1Zs.js → PlanGraphPanel-CoWJJVyD.js} +2 -2
  6. package/public/assets/{PlanGraphPanel-FfVHi1Zs.js.map → PlanGraphPanel-CoWJJVyD.js.map} +1 -1
  7. package/public/assets/{activity-CvpXBhkZ.js → activity-XqRFu_9T.js} +2 -2
  8. package/public/assets/{activity-CvpXBhkZ.js.map → activity-XqRFu_9T.js.map} +1 -1
  9. package/public/assets/{agents-D198jJhF.js → agents-BWmmWj4Q.js} +2 -2
  10. package/public/assets/{agents-D198jJhF.js.map → agents-BWmmWj4Q.js.map} +1 -1
  11. package/public/assets/{automation-BHsr7Ztm.js → automation-DRtOENs0.js} +2 -2
  12. package/public/assets/{automation-BHsr7Ztm.js.map → automation-DRtOENs0.js.map} +1 -1
  13. package/public/assets/{chat-DA5mq5zD.js → chat-gfUxbTus.js} +2 -2
  14. package/public/assets/{chat-DA5mq5zD.js.map → chat-gfUxbTus.js.map} +1 -1
  15. package/public/assets/display-CWMHll1J.js.map +1 -1
  16. package/public/assets/{formatted-body-impl-CzXDNAoZ.js → formatted-body-impl-DpNHGXXQ.js} +2 -2
  17. package/public/assets/{formatted-body-impl-CzXDNAoZ.js.map → formatted-body-impl-DpNHGXXQ.js.map} +1 -1
  18. package/public/assets/{index-y_a_P2yU.js → index-D8TVUdlC.js} +11 -11
  19. package/public/assets/index-D8TVUdlC.js.map +1 -0
  20. package/public/assets/{insights-CFWUwbIj.js → insights-DPaJ5Hme.js} +2 -2
  21. package/public/assets/{insights-CFWUwbIj.js.map → insights-DPaJ5Hme.js.map} +1 -1
  22. package/public/assets/{maintenance-B37meN9r.js → maintenance-DsszLb92.js} +2 -2
  23. package/public/assets/{maintenance-B37meN9r.js.map → maintenance-DsszLb92.js.map} +1 -1
  24. package/public/assets/{managed-agents-D_uKTfD5.js → managed-agents-DqWPxFE0.js} +2 -2
  25. package/public/assets/{managed-agents-D_uKTfD5.js.map → managed-agents-DqWPxFE0.js.map} +1 -1
  26. package/public/assets/{markdown-preview-impl-DbhvZmjh.js → markdown-preview-impl-CWR4ILHK.js} +2 -2
  27. package/public/assets/{markdown-preview-impl-DbhvZmjh.js.map → markdown-preview-impl-CWR4ILHK.js.map} +1 -1
  28. package/public/assets/{memory-CLleEaHN.js → memory-C2s9bJ38.js} +2 -2
  29. package/public/assets/{memory-CLleEaHN.js.map → memory-C2s9bJ38.js.map} +1 -1
  30. package/public/assets/{messages-CHxeZT9m.js → messages-DHRJNAvy.js} +2 -2
  31. package/public/assets/{messages-CHxeZT9m.js.map → messages-DHRJNAvy.js.map} +1 -1
  32. package/public/assets/{orchestrators-DCwgTm8t.js → orchestrators-BGNNllJc.js} +2 -2
  33. package/public/assets/{orchestrators-DCwgTm8t.js.map → orchestrators-BGNNllJc.js.map} +1 -1
  34. package/public/assets/{overview-DM4t8PeA.js → overview-C1vZfyhW.js} +2 -2
  35. package/public/assets/{overview-DM4t8PeA.js.map → overview-C1vZfyhW.js.map} +1 -1
  36. package/public/assets/{pairs-DRtIJUsl.js → pairs-C1uzo8x0.js} +2 -2
  37. package/public/assets/{pairs-DRtIJUsl.js.map → pairs-C1uzo8x0.js.map} +1 -1
  38. package/public/assets/{security-D3T8nhcY.js → security-XhAAP6q3.js} +2 -2
  39. package/public/assets/{security-D3T8nhcY.js.map → security-XhAAP6q3.js.map} +1 -1
  40. package/public/assets/{select-CGh6MQBA.js → select-DEGUFEjx.js} +2 -2
  41. package/public/assets/{select-CGh6MQBA.js.map → select-DEGUFEjx.js.map} +1 -1
  42. package/public/assets/{settings-DmEscdjG.js → settings-DTbVUdlZ.js} +2 -2
  43. package/public/assets/{settings-DmEscdjG.js.map → settings-DTbVUdlZ.js.map} +1 -1
  44. package/public/assets/{tasks-L9hPxGh2.js → tasks-zwGo7hon.js} +2 -2
  45. package/public/assets/{tasks-L9hPxGh2.js.map → tasks-zwGo7hon.js.map} +1 -1
  46. package/public/assets/{teams-Bnqfnebk.js → teams-Brc5D8ox.js} +2 -2
  47. package/public/assets/{teams-Bnqfnebk.js.map → teams-Brc5D8ox.js.map} +1 -1
  48. package/public/assets/{terminal-viewer-impl-DZ-cASX7.js → terminal-viewer-impl-C6yUAL8G.js} +2 -2
  49. package/public/assets/{terminal-viewer-impl-DZ-cASX7.js.map → terminal-viewer-impl-C6yUAL8G.js.map} +1 -1
  50. package/public/assets/{user-avatar-Gil1NpJ8.js → user-avatar-Bw1R011j.js} +2 -2
  51. package/public/assets/{user-avatar-Gil1NpJ8.js.map → user-avatar-Bw1R011j.js.map} +1 -1
  52. package/public/assets/{work-queue-DXfNzGs1.js → work-queue-ge4R7keo.js} +2 -2
  53. package/public/assets/{work-queue-DXfNzGs1.js.map → work-queue-ge4R7keo.js.map} +1 -1
  54. package/public/index.html +1 -1
  55. package/src/auth/index.ts +23 -0
  56. package/src/auth/password.ts +122 -0
  57. package/src/auth/provider.ts +60 -0
  58. package/src/auth/session.ts +92 -0
  59. package/src/db/auth-identities.ts +119 -0
  60. package/src/db/index.ts +1 -0
  61. package/src/db/migrations.ts +6 -0
  62. package/src/index.ts +6 -1
  63. package/src/routes/_shared.ts +40 -2
  64. package/src/routes/auth.ts +107 -0
  65. package/src/routes/index.ts +8 -0
  66. package/src/routes/tokens.ts +8 -2
  67. package/src/security.ts +1 -1
  68. package/src/upgrade-install-verify.ts +160 -0
  69. package/src/upgrade.ts +40 -11
  70. package/src/validation.ts +2 -0
  71. package/public/assets/index-y_a_P2yU.js.map +0 -1
@@ -1,2 +1,2 @@
1
- import{r as e}from"./chunk-CilyBKbf.js";import{Hn as t,Kn as n,Wn as r,rr as i}from"./lucide-react-CsPXvXMC.js";import{_ as a,lt as o}from"./display-CWMHll1J.js";import{C as s}from"./index-y_a_P2yU.js";var c=e(i(),1),l=r(),u={id:o};function d({user:e,address:r,distinct:i=!1,className:o}){let d=e?.avatarUrl??(e?.avatarArtifactId?`/artifacts/${encodeURIComponent(e.avatarArtifactId)}/content`:null),[f,p]=(0,c.useState)(null);(0,c.useEffect)(()=>{if(!d){p(null);return}let e=!1,t=null;return(async()=>{try{let r=await n(d);if(e)return;t=URL.createObjectURL(r),p(t)}catch{e||p(null)}})(),()=>{e=!0,t&&URL.revokeObjectURL(t)}},[d]);let m=e?.displayName||e?.handle||`User`;if(f)return(0,l.jsx)(`img`,{src:f,alt:m,title:m,className:t(`w-7 h-7 rounded-md object-cover`,o)});if(i){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:t(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(r),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
2
- //# sourceMappingURL=user-avatar-Gil1NpJ8.js.map
1
+ import{r as e}from"./chunk-CilyBKbf.js";import{Hn as t,Kn as n,Wn as r,rr as i}from"./lucide-react-CsPXvXMC.js";import{_ as a,lt as o}from"./display-CWMHll1J.js";import{C as s}from"./index-D8TVUdlC.js";var c=e(i(),1),l=r(),u={id:o};function d({user:e,address:r,distinct:i=!1,className:o}){let d=e?.avatarUrl??(e?.avatarArtifactId?`/artifacts/${encodeURIComponent(e.avatarArtifactId)}/content`:null),[f,p]=(0,c.useState)(null);(0,c.useEffect)(()=>{if(!d){p(null);return}let e=!1,t=null;return(async()=>{try{let r=await n(d);if(e)return;t=URL.createObjectURL(r),p(t)}catch{e||p(null)}})(),()=>{e=!0,t&&URL.revokeObjectURL(t)}},[d]);let m=e?.displayName||e?.handle||`User`;if(f)return(0,l.jsx)(`img`,{src:f,alt:m,title:m,className:t(`w-7 h-7 rounded-md object-cover`,o)});if(i){let e=(m.trim()[0]||`U`).toUpperCase();return(0,l.jsx)(`span`,{title:m,className:t(`inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10`,a(r),o),children:e})}return(0,l.jsx)(s,{agent:u,className:o})}export{d as t};
2
+ //# sourceMappingURL=user-avatar-Bw1R011j.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"user-avatar-Gil1NpJ8.js","names":[],"sources":["../../dashboard/src/components/shared/user-avatar.tsx"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { AgentTypeIcon } from '@/components/shared/agent-type-icon'\nimport { apiBlob } from '@/lib/api'\nimport { humanAccentClass } from '@/lib/display'\nimport { HUMAN_AGENT_ID } from '@/lib/constants'\nimport { cn } from '@/lib/utils'\nimport type { Agent, User } from '@/types'\n\n// Minimal stub so AgentTypeIcon resolves the generic 'user' face when no avatar exists.\nconst HUMAN_AGENT_STUB = { id: HUMAN_AGENT_ID } as unknown as Agent\n\n// A human's face (#390). Renders, in order of preference: the user's content-addressed avatar\n// image (the FK lands fully in #630 — until then `avatarArtifactId` is null and this path is\n// dormant), then a colored initial when we need to distinguish multiple operators, then the\n// generic operator icon. Degrades gracefully at every step so a single seeded user looks\n// exactly as before.\nexport function UserAvatar({\n user,\n address,\n distinct = false,\n className,\n}: {\n user: User | undefined\n /** The human address (`user` / `user:<id>`) — drives the deterministic accent color. */\n address?: string\n /** When true (multiple humans in view), fall back to a colored initial instead of the icon. */\n distinct?: boolean\n className?: string\n}) {\n const avatarUrl = user?.avatarUrl ?? (user?.avatarArtifactId ? `/artifacts/${encodeURIComponent(user.avatarArtifactId)}/content` : null)\n const [objectUrl, setObjectUrl] = useState<string | null>(null)\n\n useEffect(() => {\n if (!avatarUrl) { setObjectUrl(null); return }\n let cancelled = false\n let url: string | null = null\n void (async () => {\n try {\n const blob = await apiBlob(avatarUrl)\n if (cancelled) return\n url = URL.createObjectURL(blob)\n setObjectUrl(url)\n } catch {\n if (!cancelled) setObjectUrl(null)\n }\n })()\n return () => { cancelled = true; if (url) URL.revokeObjectURL(url) }\n }, [avatarUrl])\n\n const title = user?.displayName || user?.handle || 'User'\n\n if (objectUrl) {\n return <img src={objectUrl} alt={title} title={title} className={cn('w-7 h-7 rounded-md object-cover', className)} />\n }\n if (distinct) {\n const initial = (title.trim()[0] || 'U').toUpperCase()\n return (\n <span\n title={title}\n className={cn('inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10', humanAccentClass(address), className)}\n >\n {initial}\n </span>\n )\n }\n return <AgentTypeIcon agent={HUMAN_AGENT_STUB} className={className} />\n}\n"],"mappings":"+NASM,EAAmB,CAAE,GAAI,EAAgB,CAO/C,SAAgB,EAAW,CACzB,OACA,UACA,WAAW,GACX,aAQC,CACD,IAAM,EAAY,GAAM,YAAc,GAAM,iBAAmB,cAAc,mBAAmB,EAAK,iBAAiB,CAAC,UAAY,MAC7H,CAAC,EAAW,IAAA,EAAA,EAAA,UAAwC,KAAK,EAE/D,EAAA,EAAA,eAAgB,CACd,GAAI,CAAC,EAAW,CAAE,EAAa,KAAK,CAAE,OACtC,IAAI,EAAY,GACZ,EAAqB,KAWzB,OAVM,SAAY,CAChB,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAU,CACrC,GAAI,EAAW,OACf,EAAM,IAAI,gBAAgB,EAAK,CAC/B,EAAa,EAAI,MACX,CACD,GAAW,EAAa,KAAK,KAElC,KACS,CAAE,EAAY,GAAU,GAAK,IAAI,gBAAgB,EAAI,GACjE,CAAC,EAAU,CAAC,CAEf,IAAM,EAAQ,GAAM,aAAe,GAAM,QAAU,OAEnD,GAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAK,IAAK,EAAW,IAAK,EAAc,QAAO,UAAW,EAAG,kCAAmC,EAAU,CAAI,CAAA,CAEvH,GAAI,EAAU,CACZ,IAAM,GAAW,EAAM,MAAM,CAAC,IAAM,KAAK,aAAa,CACtD,OACE,EAAA,EAAA,KAAC,OAAD,CACS,QACP,UAAW,EAAG,sHAAuH,EAAiB,EAAQ,CAAE,EAAU,UAEzK,EACI,CAAA,CAGX,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,MAAO,EAA6B,YAAa,CAAA"}
1
+ {"version":3,"file":"user-avatar-Bw1R011j.js","names":[],"sources":["../../dashboard/src/components/shared/user-avatar.tsx"],"sourcesContent":["import { useEffect, useState } from 'react'\nimport { AgentTypeIcon } from '@/components/shared/agent-type-icon'\nimport { apiBlob } from '@/lib/api'\nimport { humanAccentClass } from '@/lib/display'\nimport { HUMAN_AGENT_ID } from '@/lib/constants'\nimport { cn } from '@/lib/utils'\nimport type { Agent, User } from '@/types'\n\n// Minimal stub so AgentTypeIcon resolves the generic 'user' face when no avatar exists.\nconst HUMAN_AGENT_STUB = { id: HUMAN_AGENT_ID } as unknown as Agent\n\n// A human's face (#390). Renders, in order of preference: the user's content-addressed avatar\n// image (the FK lands fully in #630 — until then `avatarArtifactId` is null and this path is\n// dormant), then a colored initial when we need to distinguish multiple operators, then the\n// generic operator icon. Degrades gracefully at every step so a single seeded user looks\n// exactly as before.\nexport function UserAvatar({\n user,\n address,\n distinct = false,\n className,\n}: {\n user: User | undefined\n /** The human address (`user` / `user:<id>`) — drives the deterministic accent color. */\n address?: string\n /** When true (multiple humans in view), fall back to a colored initial instead of the icon. */\n distinct?: boolean\n className?: string\n}) {\n const avatarUrl = user?.avatarUrl ?? (user?.avatarArtifactId ? `/artifacts/${encodeURIComponent(user.avatarArtifactId)}/content` : null)\n const [objectUrl, setObjectUrl] = useState<string | null>(null)\n\n useEffect(() => {\n if (!avatarUrl) { setObjectUrl(null); return }\n let cancelled = false\n let url: string | null = null\n void (async () => {\n try {\n const blob = await apiBlob(avatarUrl)\n if (cancelled) return\n url = URL.createObjectURL(blob)\n setObjectUrl(url)\n } catch {\n if (!cancelled) setObjectUrl(null)\n }\n })()\n return () => { cancelled = true; if (url) URL.revokeObjectURL(url) }\n }, [avatarUrl])\n\n const title = user?.displayName || user?.handle || 'User'\n\n if (objectUrl) {\n return <img src={objectUrl} alt={title} title={title} className={cn('w-7 h-7 rounded-md object-cover', className)} />\n }\n if (distinct) {\n const initial = (title.trim()[0] || 'U').toUpperCase()\n return (\n <span\n title={title}\n className={cn('inline-flex w-7 h-7 items-center justify-center rounded-md bg-muted text-xs font-semibold ring-1 ring-foreground/10', humanAccentClass(address), className)}\n >\n {initial}\n </span>\n )\n }\n return <AgentTypeIcon agent={HUMAN_AGENT_STUB} className={className} />\n}\n"],"mappings":"+NASM,EAAmB,CAAE,GAAI,EAAgB,CAO/C,SAAgB,EAAW,CACzB,OACA,UACA,WAAW,GACX,aAQC,CACD,IAAM,EAAY,GAAM,YAAc,GAAM,iBAAmB,cAAc,mBAAmB,EAAK,iBAAiB,CAAC,UAAY,MAC7H,CAAC,EAAW,IAAA,EAAA,EAAA,UAAwC,KAAK,EAE/D,EAAA,EAAA,eAAgB,CACd,GAAI,CAAC,EAAW,CAAE,EAAa,KAAK,CAAE,OACtC,IAAI,EAAY,GACZ,EAAqB,KAWzB,OAVM,SAAY,CAChB,GAAI,CACF,IAAM,EAAO,MAAM,EAAQ,EAAU,CACrC,GAAI,EAAW,OACf,EAAM,IAAI,gBAAgB,EAAK,CAC/B,EAAa,EAAI,MACX,CACD,GAAW,EAAa,KAAK,KAElC,KACS,CAAE,EAAY,GAAU,GAAK,IAAI,gBAAgB,EAAI,GACjE,CAAC,EAAU,CAAC,CAEf,IAAM,EAAQ,GAAM,aAAe,GAAM,QAAU,OAEnD,GAAI,EACF,OAAO,EAAA,EAAA,KAAC,MAAD,CAAK,IAAK,EAAW,IAAK,EAAc,QAAO,UAAW,EAAG,kCAAmC,EAAU,CAAI,CAAA,CAEvH,GAAI,EAAU,CACZ,IAAM,GAAW,EAAM,MAAM,CAAC,IAAM,KAAK,aAAa,CACtD,OACE,EAAA,EAAA,KAAC,OAAD,CACS,QACP,UAAW,EAAG,sHAAuH,EAAiB,EAAQ,CAAE,EAAU,UAEzK,EACI,CAAA,CAGX,OAAO,EAAA,EAAA,KAAC,EAAD,CAAe,MAAO,EAA6B,YAAa,CAAA"}
@@ -1,2 +1,2 @@
1
- import{Wn as e,bn as t,cn as n,i as r,ut as i}from"./lucide-react-CsPXvXMC.js";import{l as a,t as o}from"./store-DYaQAXRv.js";import{K as s,d as c,mt as l}from"./display-CWMHll1J.js";import{t as u}from"./badge-CK6ejiOh.js";import{t as d}from"./button-BAwKuRIq.js";import{B as f,K as p,s as m}from"./index-y_a_P2yU.js";import{r as h,t as g}from"./card-DDxxo-Fo.js";var _=e(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),t=o(e=>e.agentsById),n=o(e=>e.compose),r=o(e=>e.set),s=o(e=>e.doClaimTask),l=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!n.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&l(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(i,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:n.from,onChange:e=>r({compose:{...n,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:c(e)},e.id))]})]})]}),(0,_.jsx)(m,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(n=>(0,_.jsx)(b,{item:n,now:e,agentName:e=>t[e]?c(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:e,now:i,agentName:a,onClaim:o,onCancel:c,onEvents:f}){let p=l[e.severity]||l.info,m=v[e.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:e.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:e.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:e.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:e.status}),e.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),e.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:e.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[e.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),a(e.owner)]}),e.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:e.source}),(0,_.jsxs)(`span`,{title:String(e.updatedAt),children:[(0,_.jsx)(t,{className:`w-3 h-3 inline mr-1`}),s(i,e.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[e.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:c,children:[(0,_.jsx)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
- //# sourceMappingURL=work-queue-DXfNzGs1.js.map
1
+ import{Wn as e,bn as t,cn as n,i as r,ut as i}from"./lucide-react-CsPXvXMC.js";import{l as a,t as o}from"./store-DYaQAXRv.js";import{K as s,d as c,mt as l}from"./display-CWMHll1J.js";import{t as u}from"./badge-CK6ejiOh.js";import{t as d}from"./button-BAwKuRIq.js";import{B as f,K as p,s as m}from"./index-D8TVUdlC.js";import{r as h,t as g}from"./card-DDxxo-Fo.js";var _=e(),v={open:`bg-blue-500/10 text-blue-400 border-blue-500/20`,blocked:`bg-yellow-500/10 text-yellow-400 border-yellow-500/20`,claimed:`bg-emerald-500/10 text-emerald-400 border-emerald-500/20`,in_progress:`bg-purple-500/10 text-purple-400 border-purple-500/20`};function y(){let e=a(),t=o(e=>e.agentsById),n=o(e=>e.compose),r=o(e=>e.set),s=o(e=>e.doClaimTask),l=o(e=>e.doClaim),d=o(e=>e.openTaskEvents),h=o(e=>e.showError),g=o(e=>e.openConfirm),v=o(e=>e.doDeleteMessage),y=o(e=>e.doUpdateTaskStatus),x=f(),S=p(),C=S.filter(e=>e.claimable).length;function w(e){if(!n.from){h(`Validation`,`Select a "Claim as" agent first.`);return}e.sourceType===`task`&&e.task?s(e.task.id):e.sourceType===`message`&&e.message&&l(e.message.id)}function T(e){g(`Cancel Item`,`Cancel "${e.title||(e.sourceType===`task`?`Task #${e.id}`:`Message #${e.id}`)}"? This cannot be undone.`,()=>{e.sourceType===`task`&&e.task?y(e.task,`canceled`):e.sourceType===`message`&&e.message&&v(e.message.id)})}function E(e){e.task&&d(e.task)}return(0,_.jsxs)(`div`,{className:`space-y-4`,children:[(0,_.jsxs)(`div`,{className:`flex items-center justify-between flex-wrap gap-2`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(i,{className:`w-5 h-5 text-muted-foreground`}),(0,_.jsx)(`h2`,{className:`text-lg font-semibold`,children:`Work Queue`}),C>0&&(0,_.jsxs)(u,{className:`bg-orange-500/20 text-orange-400 border-orange-500/30 border`,children:[C,` claimable`]})]}),(0,_.jsxs)(`div`,{className:`flex items-center gap-2`,children:[(0,_.jsx)(`label`,{className:`text-xs text-muted-foreground shrink-0`,children:`Claim as`}),(0,_.jsxs)(`select`,{className:`rounded-md border border-input bg-background px-3 py-1.5 text-sm`,value:n.from,onChange:e=>r({compose:{...n,from:e.target.value}}),children:[(0,_.jsx)(`option`,{value:``,children:`Select agent...`}),x.map(e=>(0,_.jsx)(`option`,{value:e.id,children:c(e)},e.id))]})]})]}),(0,_.jsx)(m,{className:`h-[calc(100dvh-11rem)]`,children:(0,_.jsxs)(`div`,{className:`space-y-2 pr-2`,children:[S.length===0&&(0,_.jsx)(`div`,{className:`text-center text-muted-foreground py-16 text-sm`,children:`Work queue is empty`}),S.map(n=>(0,_.jsx)(b,{item:n,now:e,agentName:e=>t[e]?c(t[e]):e.slice(-10),onClaim:()=>w(n),onCancel:()=>T(n),onEvents:n.task?()=>E(n):void 0},n.id))]})})]})}function b({item:e,now:i,agentName:a,onClaim:o,onCancel:c,onEvents:f}){let p=l[e.severity]||l.info,m=v[e.status]||`bg-zinc-500/10 text-zinc-400 border-zinc-500/20`;return(0,_.jsx)(g,{className:e.claimable?`ring-1 ring-orange-500/30`:``,children:(0,_.jsx)(h,{className:`p-3`,children:(0,_.jsxs)(`div`,{className:`flex items-start gap-3`,children:[(0,_.jsxs)(`div`,{className:`flex-1 min-w-0`,children:[(0,_.jsxs)(`div`,{className:`flex items-center gap-2 flex-wrap mb-1`,children:[(0,_.jsx)(`span`,{className:`font-medium text-sm truncate`,children:e.title}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${p}`,children:e.severity}),(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border ${m}`,children:e.status}),e.sourceType===`message`&&(0,_.jsx)(u,{variant:`outline`,className:`text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400`,children:`msg`})]}),e.body&&(0,_.jsx)(`p`,{className:`text-xs text-muted-foreground line-clamp-2 mb-2`,children:e.body}),(0,_.jsxs)(`div`,{className:`flex items-center gap-3 text-xs text-muted-foreground flex-wrap`,children:[e.owner&&(0,_.jsxs)(`span`,{title:`Owner`,children:[(0,_.jsx)(n,{className:`w-3 h-3 inline mr-1`}),a(e.owner)]}),e.source&&(0,_.jsx)(`span`,{className:`font-mono`,children:e.source}),(0,_.jsxs)(`span`,{title:String(e.updatedAt),children:[(0,_.jsx)(t,{className:`w-3 h-3 inline mr-1`}),s(i,e.updatedAt)]})]})]}),(0,_.jsxs)(`div`,{className:`flex flex-col gap-1 shrink-0`,children:[e.claimable&&(0,_.jsxs)(d,{size:`sm`,className:`h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white`,onClick:o,children:[(0,_.jsx)(n,{className:`w-3.5 h-3.5 mr-1`}),` Claim`]}),f&&(0,_.jsx)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs`,onClick:f,children:`Events`}),(0,_.jsxs)(d,{size:`sm`,variant:`outline`,className:`h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20`,onClick:c,children:[(0,_.jsx)(r,{className:`w-3.5 h-3.5 mr-1`}),` Cancel`]})]})]})})})}export{y as WorkQueueView};
2
+ //# sourceMappingURL=work-queue-ge4R7keo.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"work-queue-DXfNzGs1.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `Task #${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"sXAWM,EAAwC,CAC5C,KAAM,kDACN,QAAS,wDACT,QAAS,2DACT,YAAa,wDACd,CAED,SAAgB,GAAgB,CAC9B,IAAM,EAAM,GAAQ,CACd,EAAa,EAAe,GAAM,EAAE,WAAW,CAC/C,EAAU,EAAe,GAAM,EAAE,QAAQ,CACzC,EAAM,EAAe,GAAM,EAAE,IAAI,CACjC,EAAc,EAAe,GAAM,EAAE,YAAY,CACjD,EAAU,EAAe,GAAM,EAAE,QAAQ,CACzC,EAAiB,EAAe,GAAM,EAAE,eAAe,CACvD,EAAY,EAAe,GAAM,EAAE,UAAU,CAC7C,EAAc,EAAe,GAAM,EAAE,YAAY,CACjD,EAAkB,EAAe,GAAM,EAAE,gBAAgB,CACzD,EAAqB,EAAe,GAAM,EAAE,mBAAmB,CAE/D,EAAgB,GAAkB,CAClC,EAAQ,GAAmB,CAE3B,EAAiB,EAAM,OAAQ,GAAM,EAAE,UAAU,CAAC,OAExD,SAAS,EAAY,EAAqB,CACxC,GAAI,CAAC,EAAQ,KAAM,CAAE,EAAU,aAAc,mCAAmC,CAAE,OAC9E,EAAK,aAAe,QAAU,EAAK,KAAM,EAAY,EAAK,KAAK,GAAG,CAC7D,EAAK,aAAe,WAAa,EAAK,SAAS,EAAQ,EAAK,QAAQ,GAAG,CAGlF,SAAS,EAAa,EAAqB,CAEzC,EAAY,cAAe,WADb,EAAK,QAAU,EAAK,aAAe,OAAS,SAAS,EAAK,KAAO,YAAY,EAAK,MACpD,+BAAkC,CACxE,EAAK,aAAe,QAAU,EAAK,KAAM,EAAmB,EAAK,KAAM,WAAW,CAC7E,EAAK,aAAe,WAAa,EAAK,SAAS,EAAgB,EAAK,QAAQ,GAAG,EACxF,CAGJ,SAAS,EAAa,EAAqB,CACrC,EAAK,MAAM,EAAe,EAAK,KAAK,CAG1C,OACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,qBAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,6DAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,mCAAf,EACE,EAAA,EAAA,KAAC,EAAD,CAAY,UAAU,gCAAkC,CAAA,EACxD,EAAA,EAAA,KAAC,KAAD,CAAI,UAAU,iCAAwB,aAAe,CAAA,CACpD,EAAiB,IAChB,EAAA,EAAA,MAAC,EAAD,CAAO,UAAU,wEAAjB,CACG,EAAe,aACV,GAEN,IACN,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,mCAAf,EACE,EAAA,EAAA,KAAC,QAAD,CAAO,UAAU,kDAAyC,WAAgB,CAAA,EAC1E,EAAA,EAAA,MAAC,SAAD,CACE,UAAU,mEACV,MAAO,EAAQ,KACf,SAAW,GAAM,EAAI,CAAE,QAAS,CAAE,GAAG,EAAS,KAAM,EAAE,OAAO,MAAO,CAAE,CAAC,UAHzE,EAKE,EAAA,EAAA,KAAC,SAAD,CAAQ,MAAM,YAAG,kBAAwB,CAAA,CACxC,EAAc,IAAK,IAAM,EAAA,EAAA,KAAC,SAAD,CAAmB,MAAO,EAAE,YAAK,EAAY,EAAE,CAAU,CAA5C,EAAE,GAA0C,CAAC,CAC7E,GACL,GACF,IAEN,EAAA,EAAA,KAAC,EAAD,CAAY,UAAU,mCACpB,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,0BAAf,CACG,EAAM,SAAW,IAChB,EAAA,EAAA,KAAC,MAAD,CAAK,UAAU,2DAAkD,sBAAyB,CAAA,CAE3F,EAAM,IAAK,IACV,EAAA,EAAA,KAAC,EAAD,CAEQ,OACD,MACL,UAAY,GAAO,EAAW,GAAM,EAAY,EAAW,GAAI,CAAG,EAAG,MAAM,IAAI,CAC/E,YAAe,EAAY,EAAK,CAChC,aAAgB,EAAa,EAAK,CAClC,SAAU,EAAK,SAAa,EAAa,EAAK,CAAG,IAAA,GACjD,CAPK,EAAK,GAOV,CACF,CACE,GACK,CAAA,CACT,GAIV,SAAS,EAAc,CACrB,OAAM,MAAK,YAAW,UAAS,WAAU,YAQxC,CACD,IAAM,EAAgB,EAAgB,EAAK,WAAa,EAAgB,KAClE,EAAc,EAAc,EAAK,SAAW,kDAElD,OACE,EAAA,EAAA,KAAC,EAAD,CAAM,UAAW,EAAK,UAAY,4BAA8B,aAC9D,EAAA,EAAA,KAAC,EAAD,CAAa,UAAU,gBACrB,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,kCAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,0BAAf,EACE,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,kDAAf,EACE,EAAA,EAAA,KAAC,OAAD,CAAM,UAAU,wCAAgC,EAAK,MAAa,CAAA,EAClE,EAAA,EAAA,KAAC,EAAD,CAAO,QAAQ,UAAU,UAAW,8BAA8B,aAAkB,EAAK,SAAiB,CAAA,EAC1G,EAAA,EAAA,KAAC,EAAD,CAAO,QAAQ,UAAU,UAAW,8BAA8B,aAAgB,EAAK,OAAe,CAAA,CACrG,EAAK,aAAe,YACnB,EAAA,EAAA,KAAC,EAAD,CAAO,QAAQ,UAAU,UAAU,uEAA8D,MAAW,CAAA,CAE1G,GACL,EAAK,OACJ,EAAA,EAAA,KAAC,IAAD,CAAG,UAAU,2DAAmD,EAAK,KAAS,CAAA,EAEhF,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,2EAAf,CACG,EAAK,QACJ,EAAA,EAAA,MAAC,OAAD,CAAM,MAAM,iBAAZ,EACE,EAAA,EAAA,KAAC,EAAD,CAAc,UAAU,sBAAwB,CAAA,CAAC,EAAU,EAAK,MAAM,CACjE,GAER,EAAK,SACJ,EAAA,EAAA,KAAC,OAAD,CAAM,UAAU,qBAAa,EAAK,OAAc,CAAA,EAElD,EAAA,EAAA,MAAC,OAAD,CAAM,MAAO,OAAO,EAAK,UAAU,UAAnC,EACE,EAAA,EAAA,KAAC,EAAD,CAAU,UAAU,sBAAwB,CAAA,CAAC,EAAQ,EAAK,EAAK,UAAU,CACpE,GACH,GACF,IACN,EAAA,EAAA,MAAC,MAAD,CAAK,UAAU,wCAAf,CACG,EAAK,YACJ,EAAA,EAAA,MAAC,EAAD,CAAQ,KAAK,KAAK,UAAU,2DAA2D,QAAS,WAAhG,EACE,EAAA,EAAA,KAAC,EAAD,CAAc,UAAU,mBAAqB,CAAA,CAAA,SACtC,GAEV,IACC,EAAA,EAAA,KAAC,EAAD,CAAQ,KAAK,KAAK,QAAQ,UAAU,UAAU,cAAc,QAAS,WAAU,SAEtE,CAAA,EAEX,EAAA,EAAA,MAAC,EAAD,CAAQ,KAAK,KAAK,QAAQ,UAAU,UAAU,oFAAoF,QAAS,WAA3I,EACE,EAAA,EAAA,KAAC,EAAD,CAAG,UAAU,mBAAqB,CAAA,CAAA,UAC3B,GACL,GACF,GACM,CAAA,CACT,CAAA"}
1
+ {"version":3,"file":"work-queue-ge4R7keo.js","names":[],"sources":["../../dashboard/src/components/views/work-queue.tsx"],"sourcesContent":["import { useRelayStore, useNow } from '@/store'\nimport { useWorkQueueItems, useComposeAgents } from '@/hooks/use-selectors'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { ScrollArea } from '@/components/ui/scroll-area'\nimport { ListChecks, CheckCircle2, Calendar, X } from 'lucide-react'\nimport { displayName, timeAgo } from '@/lib/display'\nimport { SEVERITY_COLORS } from '@/lib/constants'\nimport type { WorkQueueItem } from '@/types'\n\nconst STATUS_COLORS: Record<string, string> = {\n open: 'bg-blue-500/10 text-blue-400 border-blue-500/20',\n blocked: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',\n claimed: 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20',\n in_progress: 'bg-purple-500/10 text-purple-400 border-purple-500/20',\n}\n\nexport function WorkQueueView() {\n const now = useNow()\n const agentsById = useRelayStore((s) => s.agentsById)\n const compose = useRelayStore((s) => s.compose)\n const set = useRelayStore((s) => s.set)\n const doClaimTask = useRelayStore((s) => s.doClaimTask)\n const doClaim = useRelayStore((s) => s.doClaim)\n const openTaskEvents = useRelayStore((s) => s.openTaskEvents)\n const showError = useRelayStore((s) => s.showError)\n const openConfirm = useRelayStore((s) => s.openConfirm)\n const doDeleteMessage = useRelayStore((s) => s.doDeleteMessage)\n const doUpdateTaskStatus = useRelayStore((s) => s.doUpdateTaskStatus)\n\n const composeAgents = useComposeAgents()\n const items = useWorkQueueItems()\n\n const claimableCount = items.filter((i) => i.claimable).length\n\n function handleClaim(item: WorkQueueItem) {\n if (!compose.from) { showError('Validation', 'Select a \"Claim as\" agent first.'); return }\n if (item.sourceType === 'task' && item.task) doClaimTask(item.task.id)\n else if (item.sourceType === 'message' && item.message) doClaim(item.message.id)\n }\n\n function handleCancel(item: WorkQueueItem) {\n const label = item.title || (item.sourceType === 'task' ? `Task #${item.id}` : `Message #${item.id}`)\n openConfirm('Cancel Item', `Cancel \"${label}\"? This cannot be undone.`, () => {\n if (item.sourceType === 'task' && item.task) doUpdateTaskStatus(item.task, 'canceled')\n else if (item.sourceType === 'message' && item.message) doDeleteMessage(item.message.id)\n })\n }\n\n function handleEvents(item: WorkQueueItem) {\n if (item.task) openTaskEvents(item.task)\n }\n\n return (\n <div className=\"space-y-4\">\n <div className=\"flex items-center justify-between flex-wrap gap-2\">\n <div className=\"flex items-center gap-2\">\n <ListChecks className=\"w-5 h-5 text-muted-foreground\" />\n <h2 className=\"text-lg font-semibold\">Work Queue</h2>\n {claimableCount > 0 && (\n <Badge className=\"bg-orange-500/20 text-orange-400 border-orange-500/30 border\">\n {claimableCount} claimable\n </Badge>\n )}\n </div>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-xs text-muted-foreground shrink-0\">Claim as</label>\n <select\n className=\"rounded-md border border-input bg-background px-3 py-1.5 text-sm\"\n value={compose.from}\n onChange={(e) => set({ compose: { ...compose, from: e.target.value } })}\n >\n <option value=\"\">Select agent...</option>\n {composeAgents.map((a) => <option key={a.id} value={a.id}>{displayName(a)}</option>)}\n </select>\n </div>\n </div>\n\n <ScrollArea className=\"h-[calc(100dvh-11rem)]\">\n <div className=\"space-y-2 pr-2\">\n {items.length === 0 && (\n <div className=\"text-center text-muted-foreground py-16 text-sm\">Work queue is empty</div>\n )}\n {items.map((item) => (\n <WorkQueueCard\n key={item.id}\n item={item}\n now={now}\n agentName={(id) => agentsById[id] ? displayName(agentsById[id]) : id.slice(-10)}\n onClaim={() => handleClaim(item)}\n onCancel={() => handleCancel(item)}\n onEvents={item.task ? () => handleEvents(item) : undefined}\n />\n ))}\n </div>\n </ScrollArea>\n </div>\n )\n}\n\nfunction WorkQueueCard({\n item, now, agentName, onClaim, onCancel, onEvents,\n}: {\n item: WorkQueueItem\n now: number\n agentName: (id: string) => string\n onClaim: () => void\n onCancel: () => void\n onEvents?: () => void\n}) {\n const severityClass = SEVERITY_COLORS[item.severity] || SEVERITY_COLORS.info\n const statusClass = STATUS_COLORS[item.status] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20'\n\n return (\n <Card className={item.claimable ? 'ring-1 ring-orange-500/30' : ''}>\n <CardContent className=\"p-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 flex-wrap mb-1\">\n <span className=\"font-medium text-sm truncate\">{item.title}</span>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${severityClass}`}>{item.severity}</Badge>\n <Badge variant=\"outline\" className={`text-xs px-1.5 py-0 border ${statusClass}`}>{item.status}</Badge>\n {item.sourceType === 'message' && (\n <Badge variant=\"outline\" className=\"text-xs px-1.5 py-0 border border-zinc-500/20 text-zinc-400\">msg</Badge>\n )}\n </div>\n {item.body && (\n <p className=\"text-xs text-muted-foreground line-clamp-2 mb-2\">{item.body}</p>\n )}\n <div className=\"flex items-center gap-3 text-xs text-muted-foreground flex-wrap\">\n {item.owner && (\n <span title=\"Owner\">\n <CheckCircle2 className=\"w-3 h-3 inline mr-1\" />{agentName(item.owner)}\n </span>\n )}\n {item.source && (\n <span className=\"font-mono\">{item.source}</span>\n )}\n <span title={String(item.updatedAt)}>\n <Calendar className=\"w-3 h-3 inline mr-1\" />{timeAgo(now, item.updatedAt)}\n </span>\n </div>\n </div>\n <div className=\"flex flex-col gap-1 shrink-0\">\n {item.claimable && (\n <Button size=\"sm\" className=\"h-7 text-xs bg-orange-600 hover:bg-orange-700 text-white\" onClick={onClaim}>\n <CheckCircle2 className=\"w-3.5 h-3.5 mr-1\" /> Claim\n </Button>\n )}\n {onEvents && (\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs\" onClick={onEvents}>\n Events\n </Button>\n )}\n <Button size=\"sm\" variant=\"outline\" className=\"h-7 text-xs text-red-400 hover:text-red-300 hover:bg-red-500/10 border-red-500/20\" onClick={onCancel}>\n <X className=\"w-3.5 h-3.5 mr-1\" /> Cancel\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n"],"mappings":"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"}
package/public/index.html CHANGED
@@ -12,7 +12,7 @@
12
12
  <link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%2309090b'/%3E%3Ccircle cx='16' cy='16' r='4.5' fill='%2358a6ff'/%3E%3Ccircle cx='6' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='8' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='6' cy='24' r='2.5' fill='%233fb950'/%3E%3Ccircle cx='26' cy='24' r='2.5' fill='%233fb950'/%3E%3Cline x1='8' y1='9.5' x2='13' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='9.5' x2='19' y2='14' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='8' y1='22.5' x2='13' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3Cline x1='24' y1='22.5' x2='19' y2='18' stroke='%2330363d' stroke-width='1.5'/%3E%3C/svg%3E">
13
13
  <link rel="manifest" href="manifest.webmanifest">
14
14
  <link rel="apple-touch-icon" href="icons/agent-relay-192.png">
15
- <script type="module" crossorigin src="./assets/index-y_a_P2yU.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-D8TVUdlC.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/chunk-CilyBKbf.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/preload-helper-CP_edrrL.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/lucide-react-CsPXvXMC.js">
@@ -0,0 +1,23 @@
1
+ // Composition root for the auth module (#389). Importing this module registers the built-in
2
+ // providers (password in Phase 1; GitHub OAuth joins in Phase 2) and re-exports the public auth API.
3
+ // Consumers import from here so the built-ins are guaranteed registered before any login route runs.
4
+ // This is the ONLY place that wires concrete providers into the registry, so src/auth/provider.ts
5
+ // (the registry) stays free of any dependency on a concrete provider.
6
+ import { registerAuthProvider } from "./provider.ts";
7
+ import { passwordAuthProvider } from "./password.ts";
8
+
9
+ let builtInsRegistered = false;
10
+
11
+ /** Register the built-in auth providers. Idempotent — safe to call from boot and from tests. */
12
+ export function registerBuiltInAuthProviders(): void {
13
+ if (builtInsRegistered) return;
14
+ builtInsRegistered = true;
15
+ registerAuthProvider(passwordAuthProvider);
16
+ }
17
+
18
+ // Side-effect on import: the built-ins are live as soon as anything pulls in the auth module.
19
+ registerBuiltInAuthProviders();
20
+
21
+ export * from "./provider.ts";
22
+ export * from "./password.ts";
23
+ export * from "./session.ts";
@@ -0,0 +1,122 @@
1
+ // Password authentication provider (#389). The only built-in provider in Phase 1. Secrets are
2
+ // hashed with scrypt (node:crypto — no dependency) and compared with `timingSafeEqual`, the same
3
+ // constant-time primitive the token layer uses. The hash is stored in `auth_identities.secret_hash`
4
+ // keyed by (provider="password", provider_subject=<username>).
5
+ import { randomBytes, scryptSync, timingSafeEqual } from "node:crypto";
6
+ import { createAuthIdentity, getAuthIdentity, type AuthIdentity } from "../db/auth-identities.ts";
7
+ import { isRecord } from "agent-relay-sdk";
8
+ import type { AuthProvider, AuthVerifyResult } from "./provider.ts";
9
+
10
+ /** The provider id stored in `auth_identities.provider` and sent in a login request's `provider`. */
11
+ export const PASSWORD_PROVIDER_ID = "password";
12
+
13
+ // scrypt cost parameters. N=2^14 keeps 128*N*r ≈ 16MB under node's 32MB default maxmem; r=8/p=1 are
14
+ // the standard interactive-login settings. keylen 64. Encoded into the hash string so a future cost
15
+ // bump stays verifiable against existing hashes.
16
+ const SCRYPT_N = 16384;
17
+ const SCRYPT_R = 8;
18
+ const SCRYPT_P = 1;
19
+ const KEYLEN = 64;
20
+ const SALT_BYTES = 16;
21
+ export const DUMMY_PASSWORD_HASH =
22
+ `scrypt$${SCRYPT_N}$${SCRYPT_R}$${SCRYPT_P}$${"00".repeat(SALT_BYTES)}$${"00".repeat(KEYLEN)}`;
23
+
24
+ type PasswordVerifier = (password: string, stored: string | null) => boolean;
25
+ type PasswordIdentityLookup = (provider: string, providerSubject: string) => AuthIdentity | undefined;
26
+
27
+ interface PasswordVerifyAttempt {
28
+ valid: boolean;
29
+ attemptedKdf: boolean;
30
+ }
31
+
32
+ /** Hash a plaintext password into a self-describing `scrypt$N$r$p$saltHex$hashHex` string. */
33
+ export function hashPassword(password: string): string {
34
+ const salt = randomBytes(SALT_BYTES);
35
+ const derived = scryptSync(password, salt, KEYLEN, { N: SCRYPT_N, r: SCRYPT_R, p: SCRYPT_P });
36
+ return `scrypt$${SCRYPT_N}$${SCRYPT_R}$${SCRYPT_P}$${salt.toString("hex")}$${derived.toString("hex")}`;
37
+ }
38
+
39
+ function verifyPasswordAttempt(password: string, stored: string | null): PasswordVerifyAttempt {
40
+ if (!stored) return { valid: false, attemptedKdf: false };
41
+ const parts = stored.split("$");
42
+ if (parts.length !== 6 || parts[0] !== "scrypt") return { valid: false, attemptedKdf: false };
43
+ const [, nRaw, rRaw, pRaw, saltHex, hashHex] = parts as [string, string, string, string, string, string];
44
+ const N = Number(nRaw);
45
+ const r = Number(rRaw);
46
+ const p = Number(pRaw);
47
+ if (!Number.isInteger(N) || !Number.isInteger(r) || !Number.isInteger(p)) return { valid: false, attemptedKdf: false };
48
+ const expected = Buffer.from(hashHex, "hex");
49
+ if (expected.length <= 0) return { valid: false, attemptedKdf: false };
50
+ let derived: Buffer;
51
+ try {
52
+ derived = scryptSync(password, Buffer.from(saltHex, "hex"), expected.length, { N, r, p });
53
+ } catch {
54
+ return { valid: false, attemptedKdf: false };
55
+ }
56
+ return { valid: derived.length === expected.length && timingSafeEqual(derived, expected), attemptedKdf: true };
57
+ }
58
+
59
+ /** Constant-time verify a plaintext password against a stored `scrypt$...` hash. */
60
+ export function verifyPassword(password: string, stored: string | null): boolean {
61
+ return verifyPasswordAttempt(password, stored).valid;
62
+ }
63
+
64
+ function verifyPasswordWithDummyFallback(password: string, stored: string | null): boolean {
65
+ const result = verifyPasswordAttempt(password, stored);
66
+ if (result.attemptedKdf) return result.valid;
67
+ verifyPasswordAttempt(password, DUMMY_PASSWORD_HASH);
68
+ return false;
69
+ }
70
+
71
+ /**
72
+ * Link a password login to a user (admin pre-link / first-user bootstrap). Hashes the password and
73
+ * inserts an `auth_identities` row. `username` is the `provider_subject` the user logs in with.
74
+ */
75
+ export function createPasswordIdentity(input: {
76
+ userId: string;
77
+ username: string;
78
+ password: string;
79
+ metadata?: Record<string, unknown>;
80
+ }): AuthIdentity {
81
+ return createAuthIdentity({
82
+ userId: input.userId,
83
+ provider: PASSWORD_PROVIDER_ID,
84
+ providerSubject: input.username,
85
+ secretHash: hashPassword(input.password),
86
+ metadata: input.metadata,
87
+ });
88
+ }
89
+
90
+ /** Parse `{ username, password }` from a login request body. */
91
+ function readCredentials(credentials: unknown): { username: string; password: string } | null {
92
+ if (!isRecord(credentials)) return null;
93
+ const username = credentials.username ?? credentials.handle ?? credentials.email;
94
+ const password = credentials.password;
95
+ if (typeof username !== "string" || !username || typeof password !== "string" || !password) return null;
96
+ return { username, password };
97
+ }
98
+
99
+ export function verifyPasswordCredentials(
100
+ credentials: unknown,
101
+ opts: { lookupIdentity?: PasswordIdentityLookup; verifyStoredPassword?: PasswordVerifier } = {},
102
+ ): AuthVerifyResult | null {
103
+ const creds = readCredentials(credentials);
104
+ if (!creds) return null;
105
+ const lookupIdentity = opts.lookupIdentity ?? getAuthIdentity;
106
+ const verifyStoredPassword = opts.verifyStoredPassword ?? verifyPasswordWithDummyFallback;
107
+ const identity = lookupIdentity(PASSWORD_PROVIDER_ID, creds.username);
108
+ const passwordMatches = verifyStoredPassword(creds.password, identity?.secretHash ?? DUMMY_PASSWORD_HASH);
109
+ if (!identity || !passwordMatches) return null;
110
+ return { providerSubject: creds.username };
111
+ }
112
+
113
+ export const passwordAuthProvider: AuthProvider = {
114
+ id: PASSWORD_PROVIDER_ID,
115
+ displayName: "Password",
116
+ flow: "password",
117
+ verify(credentials: unknown): AuthVerifyResult | null {
118
+ // Unknown username, malformed stored hash, and wrong password all pay one scrypt verification
119
+ // before returning null, so login failure timing does not identify linked accounts.
120
+ return verifyPasswordCredentials(credentials);
121
+ },
122
+ };
@@ -0,0 +1,60 @@
1
+ // Pluggable authentication providers (#389). An AuthProvider turns a login request into a
2
+ // `providerSubject` — the stable external identifier (a username, an OAuth `sub`, an email) that
3
+ // `auth_identities` maps onto a `users.id`. The login route is provider-agnostic: it resolves the
4
+ // provider, calls `verify()`, then looks the returned subject up in `auth_identities`. Adding a
5
+ // provider (GitHub OAuth in Phase 2, magic-link, …) is a `register()` call plus rows — no schema
6
+ // change, no route change (the pluggability guarantee #389 must hold).
7
+ //
8
+ // In-process registry. The registry knows ONLY the interface — concrete providers register
9
+ // themselves via the composition root (src/auth/index.ts), which keeps this module free of any
10
+ // dependency on a concrete provider (no registry ↔ plugin cycle).
11
+
12
+ /** How a provider proves identity — drives the login UI and which fields `verify()` reads. */
13
+ export type AuthProviderFlow = "password" | "oauth" | "magic-link";
14
+
15
+ /** Result of a successful `verify()`: the external subject to resolve against `auth_identities`. */
16
+ export interface AuthVerifyResult {
17
+ providerSubject: string;
18
+ /** Optional provider-supplied profile bits (display name, email) for first-link/auto-provision. */
19
+ profile?: Record<string, unknown>;
20
+ }
21
+
22
+ export interface AuthProvider {
23
+ /** Stable id; the `auth_identities.provider` value and the `provider` field a login request sends. */
24
+ id: string;
25
+ /** Human label for the login UI. */
26
+ displayName: string;
27
+ flow: AuthProviderFlow;
28
+ /**
29
+ * Begin an interactive (redirect) flow — returns where to send the user (e.g. an OAuth authorize
30
+ * URL). Optional: password is single-step and omits it. Phase 2 (GitHub OAuth) implements it.
31
+ */
32
+ begin?(req: Request): Promise<{ redirectUrl: string } | null> | ({ redirectUrl: string } | null);
33
+ /**
34
+ * Verify the presented credentials and return the authenticated subject, or null on failure.
35
+ * MUST be constant-ish on failure (no "unknown user" vs "bad password" distinction leaking to
36
+ * the caller — both return null). The route maps a non-null subject to a user.
37
+ */
38
+ verify(credentials: unknown): Promise<AuthVerifyResult | null> | (AuthVerifyResult | null);
39
+ }
40
+
41
+ const registry = new Map<string, AuthProvider>();
42
+
43
+ /** Register (or replace) a provider. Idempotent by id — re-registering overrides, which is how
44
+ * tests install a throwaway provider to prove pluggability. */
45
+ export function registerAuthProvider(provider: AuthProvider): void {
46
+ registry.set(provider.id, provider);
47
+ }
48
+
49
+ /** Remove a provider by id (test teardown / runtime disable). Returns true if one was removed. */
50
+ export function unregisterAuthProvider(id: string): boolean {
51
+ return registry.delete(id);
52
+ }
53
+
54
+ export function getAuthProvider(id: string): AuthProvider | undefined {
55
+ return registry.get(id);
56
+ }
57
+
58
+ export function listAuthProviders(): AuthProvider[] {
59
+ return [...registry.values()];
60
+ }
@@ -0,0 +1,92 @@
1
+ // Human session tokens (#389). A session is a component token minted via the existing `createToken`
2
+ // — the `tokens` table IS the session store, so `verifyComponentToken` already enforces expiry and
3
+ // revocation, and "sign out everywhere" is just revoking every token for a `sub`. The session token
4
+ // reuses ONLY the signing primitive: it carries a new `sub` namespace (`user:<id>`) and a new
5
+ // `role` ("user"), so no machine-token path branches on it.
6
+ import { userSinkId, userIdFromAddress, isHumanAgentId, type User, type UserRole } from "agent-relay-sdk";
7
+ import { createToken } from "../token-db.ts";
8
+ import { verifyComponentToken } from "../security.ts";
9
+ import { getUser } from "../db/users.ts";
10
+ import type { ComponentToken } from "../types.ts";
11
+
12
+ /** Access token lifetime (~1h) — short, because refresh re-mints it silently. */
13
+ export const ACCESS_TTL_SECONDS = 60 * 60;
14
+ /** Refresh token lifetime (~30d) — the dashboard re-mints the access token until this expires. */
15
+ export const REFRESH_TTL_SECONDS = 30 * 24 * 60 * 60;
16
+ /** Marker scope carried ONLY by refresh tokens. It matches no route's required scope, so a refresh
17
+ * token can never act as an access token; the refresh route validates it explicitly. */
18
+ export const REFRESH_SCOPE = "auth:refresh";
19
+
20
+ /** The `role` stamped on a human session token. New namespace — no machine path branches on it. */
21
+ export const USER_TOKEN_ROLE = "user";
22
+
23
+ /**
24
+ * Map a user role to the scopes its session carries. STOPGAP for #392 (real RBAC): admins get full
25
+ * access via the `admin:*` alias (→ `system:admin`, which `hasScope` short-circuits everywhere), and
26
+ * everyone else gets a read-only baseline. When #392 lands the per-role scope matrix, replace this
27
+ * one function — every session inherits the new policy with no other change.
28
+ */
29
+ export function scopesForUserRole(role: UserRole): string[] {
30
+ if (role === "admin") return ["admin:*"];
31
+ if (role === "operator") return ["agent:read", "agent:write", "message:read", "message:send", "task:read", "task:write"];
32
+ return ["agent:read", "message:read", "task:read"];
33
+ }
34
+
35
+ export interface MintedSession {
36
+ accessToken: string;
37
+ refreshToken: string;
38
+ accessJti: string;
39
+ refreshJti: string;
40
+ expiresIn: number;
41
+ }
42
+
43
+ /**
44
+ * Mint an access + refresh token pair for a logged-in user. `constraints.tenantId` carries the
45
+ * SaaS seam (identity-only — enforces nothing). Both tokens share the `user:<id>` sub so a single
46
+ * "sign out everywhere" revoke (by sub) kills the whole session family.
47
+ */
48
+ export function mintUserSession(user: User): MintedSession {
49
+ const sub = userSinkId(user.id);
50
+ const constraints = user.tenantId ? { tenantId: user.tenantId } : undefined;
51
+ const access = createToken({
52
+ sub,
53
+ role: USER_TOKEN_ROLE,
54
+ scope: scopesForUserRole(user.role),
55
+ constraints,
56
+ ttlSeconds: ACCESS_TTL_SECONDS,
57
+ createdBy: `auth:login:${user.id}`,
58
+ });
59
+ const refresh = createToken({
60
+ sub,
61
+ role: USER_TOKEN_ROLE,
62
+ scope: [REFRESH_SCOPE],
63
+ constraints,
64
+ ttlSeconds: REFRESH_TTL_SECONDS,
65
+ createdBy: `auth:refresh:${user.id}`,
66
+ });
67
+ return {
68
+ accessToken: access.token,
69
+ refreshToken: refresh.token,
70
+ accessJti: access.record.jti,
71
+ refreshJti: refresh.record.jti,
72
+ expiresIn: ACCESS_TTL_SECONDS,
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Validate a refresh token and resolve it to its live user. Returns null unless the token verifies
78
+ * (signature + unexpired + unrevoked, via `verifyComponentToken`), carries the refresh scope, names
79
+ * a human `sub`, and that user still exists and is active. The caller mints a fresh session for the
80
+ * user.
81
+ */
82
+ export function resolveRefreshToken(token: string): { user: User; payload: ComponentToken } | null {
83
+ const payload = verifyComponentToken(token);
84
+ if (!payload) return null;
85
+ if (!payload.scope.includes(REFRESH_SCOPE)) return null;
86
+ if (!isHumanAgentId(payload.sub)) return null;
87
+ const userId = userIdFromAddress(payload.sub);
88
+ if (!userId) return null;
89
+ const user = getUser(userId);
90
+ if (!user || user.status !== "active") return null;
91
+ return { user, payload };
92
+ }
@@ -0,0 +1,119 @@
1
+ // Pluggable authentication identities (#389 auth & sessions). Maps an external login —
2
+ // a (provider, provider_subject) pair such as ("password", "admin") or ("github", "12345") —
3
+ // onto a relational `users.id`. The login flow authenticates a provider_subject, looks it up
4
+ // here, and mints a session token for the resolved user. Provider-agnostic by construction:
5
+ // `secret_hash` is nullable so an OAuth/magic-link identity is just a row with no stored secret —
6
+ // a new provider needs ZERO schema change (the #389 pluggability guarantee).
7
+ //
8
+ // DDL home (single owner; the barrel re-exports, never inlines DDL). Wired into applyMigrations
9
+ // right after seedDefaultUser(), mirroring the idempotent ensureUsersSchema() pattern.
10
+ import { randomUUID } from "node:crypto";
11
+ import { getDb } from "./connection.ts";
12
+
13
+ export interface AuthIdentity {
14
+ id: string;
15
+ userId: string;
16
+ provider: string;
17
+ providerSubject: string;
18
+ /** Opaque provider-managed secret material (e.g. a scrypt password hash). Null for secretless
19
+ * providers (OAuth, magic-link) whose proof lives with the upstream provider, not here. */
20
+ secretHash: string | null;
21
+ metadata: Record<string, unknown>;
22
+ createdAt: number;
23
+ }
24
+
25
+ interface AuthIdentityRow {
26
+ id: string;
27
+ user_id: string;
28
+ provider: string;
29
+ provider_subject: string;
30
+ secret_hash: string | null;
31
+ metadata: string | null;
32
+ created_at: number;
33
+ }
34
+
35
+ /**
36
+ * Single DDL home for `auth_identities` (#389). Idempotent (CREATE TABLE IF NOT EXISTS), so it is
37
+ * safe on every boot/migration — an old single-user DB gains the table on upgrade, with zero rows,
38
+ * and the god-token break-glass keeps working without any identity present. `UNIQUE(provider,
39
+ * provider_subject)` makes a login subject resolve to exactly one identity; `user_id` cascades on
40
+ * user delete so removing a human drops their logins.
41
+ */
42
+ export function ensureAuthIdentitiesSchema(): void {
43
+ getDb().run(`
44
+ CREATE TABLE IF NOT EXISTS auth_identities (
45
+ id TEXT PRIMARY KEY,
46
+ user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
47
+ provider TEXT NOT NULL,
48
+ provider_subject TEXT NOT NULL,
49
+ secret_hash TEXT,
50
+ metadata TEXT NOT NULL DEFAULT '{}',
51
+ created_at INTEGER NOT NULL,
52
+ UNIQUE(provider, provider_subject)
53
+ )
54
+ `);
55
+ getDb().run("CREATE INDEX IF NOT EXISTS idx_auth_identities_user ON auth_identities(user_id)");
56
+ }
57
+
58
+ function rowToIdentity(row: AuthIdentityRow): AuthIdentity {
59
+ return {
60
+ id: row.id,
61
+ userId: row.user_id,
62
+ provider: row.provider,
63
+ providerSubject: row.provider_subject,
64
+ secretHash: row.secret_hash,
65
+ metadata: row.metadata ? (JSON.parse(row.metadata) as Record<string, unknown>) : {},
66
+ createdAt: row.created_at,
67
+ };
68
+ }
69
+
70
+ /** Resolve a login subject to its identity, or undefined if no such (provider, subject) exists. */
71
+ export function getAuthIdentity(provider: string, providerSubject: string): AuthIdentity | undefined {
72
+ const row = getDb()
73
+ .query("SELECT * FROM auth_identities WHERE provider = ? AND provider_subject = ?")
74
+ .get(provider, providerSubject) as AuthIdentityRow | null;
75
+ return row ? rowToIdentity(row) : undefined;
76
+ }
77
+
78
+ /** All identities linked to a user — used by account UIs and "unlink provider" flows. */
79
+ export function listAuthIdentitiesForUser(userId: string): AuthIdentity[] {
80
+ const rows = getDb()
81
+ .query("SELECT * FROM auth_identities WHERE user_id = ? ORDER BY created_at")
82
+ .all(userId) as AuthIdentityRow[];
83
+ return rows.map(rowToIdentity);
84
+ }
85
+
86
+ /**
87
+ * Link a login to a user. `secretHash` is null for secretless providers. Throws on a duplicate
88
+ * (provider, provider_subject) — the caller surfaces it as a 409/validation error.
89
+ */
90
+ export function createAuthIdentity(input: {
91
+ userId: string;
92
+ provider: string;
93
+ providerSubject: string;
94
+ secretHash?: string | null;
95
+ metadata?: Record<string, unknown>;
96
+ }): AuthIdentity {
97
+ const id = randomUUID();
98
+ const now = Date.now();
99
+ getDb()
100
+ .query(
101
+ `INSERT INTO auth_identities (id, user_id, provider, provider_subject, secret_hash, metadata, created_at)
102
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
103
+ )
104
+ .run(
105
+ id,
106
+ input.userId,
107
+ input.provider,
108
+ input.providerSubject,
109
+ input.secretHash ?? null,
110
+ JSON.stringify(input.metadata ?? {}),
111
+ now,
112
+ );
113
+ return getAuthIdentity(input.provider, input.providerSubject)!;
114
+ }
115
+
116
+ /** Remove an identity by id. Returns true if a row was deleted. */
117
+ export function deleteAuthIdentity(id: string): boolean {
118
+ return getDb().query("DELETE FROM auth_identities WHERE id = ?").run(id).changes > 0;
119
+ }
package/src/db/index.ts CHANGED
@@ -17,6 +17,7 @@ export * from "./delivery.ts";
17
17
  export * from "./message-reads.ts";
18
18
  export * from "./inbox.ts";
19
19
  export * from "./users.ts";
20
+ export * from "./auth-identities.ts";
20
21
  export * from "./agent-ownership.ts";
21
22
  export * from "./drive-leases.ts";
22
23
  export * from "./activity.ts";
@@ -16,6 +16,7 @@ import { STALE_TTL_MS, DAY_MS, CLAIM_LEASE_MS, POOL_CLAIM_LEASE_MS, WORKSPACE_ME
16
16
  import { matchAgents } from "../agent-ref";
17
17
  import { getDb } from "./connection.ts";
18
18
  import { ensureUsersSchema, seedDefaultUser } from "./users.ts";
19
+ import { ensureAuthIdentitiesSchema } from "./auth-identities.ts";
19
20
  import { ensureAgentOwnershipSchema, seedDefaultAgentOwnership } from "./agent-ownership.ts";
20
21
  import { ensureDriveLeaseSchema } from "./drive-leases.ts";
21
22
  import { ensureContinuationArchiveSearchSchema } from "./continuation-archives.ts";
@@ -800,6 +801,11 @@ export function applyMigrations(): void {
800
801
  ensureUsersSchema();
801
802
  seedDefaultUser();
802
803
 
804
+ // Pluggable login identities (#389): the (provider, provider_subject) → users.id map behind real
805
+ // login + sessions. Ensured right after the user seed (FK → users) and idempotent — an old
806
+ // single-user DB gains it empty, and the god-token break-glass keeps working with zero rows.
807
+ ensureAuthIdentitiesSchema();
808
+
803
809
  // Agent ownership/visibility table (#392). FK-references both agents and users, so it ensures
804
810
  // AFTER both exist. The backfill (seedDefaultAgentOwnership) runs below, once the built-in
805
811
  // agents are in, so they can be excluded from ownership.
package/src/index.ts CHANGED
@@ -268,7 +268,12 @@ export function createFetchHandler(
268
268
  const matched = matchRoute(req.method, url.pathname);
269
269
  if (matched) {
270
270
  const publicPaths = ["/api/spec", "/api/docs", "/api/health"];
271
- if (!publicPaths.includes(url.pathname)) {
271
+ // Pre-auth allowlist (#389): the auth endpoints must be reachable unauthenticated — they ARE
272
+ // the way a session is obtained. Each handler self-authenticates (login verifies credentials,
273
+ // refresh validates the refresh token, logout revokes the presented token). Everything else
274
+ // still flows through the component-token gate below.
275
+ const publicAuthPaths = ["/api/auth/providers", "/api/auth/login", "/api/auth/refresh", "/api/auth/logout"];
276
+ if (!publicPaths.includes(url.pathname) && !publicAuthPaths.includes(url.pathname)) {
272
277
  const integrationAuth = getIntegrationAuth(req);
273
278
  const componentAuth = getComponentAuth(req);
274
279
  const handlerOwnsScopedAuth = url.pathname === "/api/tokens/me" ||