heartbeads 0.4.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.
- package/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +49 -0
- package/.next/app-path-routes-manifest.json +1 -0
- package/.next/build-manifest.json +32 -0
- package/.next/export-marker.json +1 -0
- package/.next/images-manifest.json +1 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +1 -0
- package/.next/react-loadable-manifest.json +8 -0
- package/.next/required-server-files.json +1 -0
- package/.next/routes-manifest.json +1 -0
- package/.next/server/app/_not-found/page.js +1 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +6 -0
- package/.next/server/app/_not-found.rsc +9 -0
- package/.next/server/app/api/auth/route.js +1 -0
- package/.next/server/app/api/auth/route.js.nft.json +1 -0
- package/.next/server/app/api/beads/route.js +8 -0
- package/.next/server/app/api/beads/route.js.nft.json +1 -0
- package/.next/server/app/api/beads/stream/route.js +10 -0
- package/.next/server/app/api/beads/stream/route.js.nft.json +1 -0
- package/.next/server/app/api/beads.body +1 -0
- package/.next/server/app/api/beads.meta +1 -0
- package/.next/server/app/api/config/route.js +8 -0
- package/.next/server/app/api/config/route.js.nft.json +1 -0
- package/.next/server/app/api/docs/page.js +120 -0
- package/.next/server/app/api/docs/page.js.nft.json +1 -0
- package/.next/server/app/api/docs/page_client-reference-manifest.js +1 -0
- package/.next/server/app/api/docs.html +120 -0
- package/.next/server/app/api/docs.meta +5 -0
- package/.next/server/app/api/docs.rsc +70 -0
- package/.next/server/app/api/login/route.js +1 -0
- package/.next/server/app/api/login/route.js.nft.json +1 -0
- package/.next/server/app/api/logout/route.js +1 -0
- package/.next/server/app/api/logout/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/callback/route.js +1 -0
- package/.next/server/app/api/oauth/callback/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/client-metadata.json/route.js +1 -0
- package/.next/server/app/api/oauth/client-metadata.json/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/jwks.json/route.js +1 -0
- package/.next/server/app/api/oauth/jwks.json/route.js.nft.json +1 -0
- package/.next/server/app/api/records/route.js +1 -0
- package/.next/server/app/api/records/route.js.nft.json +1 -0
- package/.next/server/app/api/status/route.js +1 -0
- package/.next/server/app/api/status/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/graph/route.js +1 -0
- package/.next/server/app/api/v1/graph/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/issues/[id]/route.js +1 -0
- package/.next/server/app/api/v1/issues/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/ready/route.js +1 -0
- package/.next/server/app/api/v1/ready/route.js.nft.json +1 -0
- package/.next/server/app/index.html +1 -0
- package/.next/server/app/index.meta +5 -0
- package/.next/server/app/index.rsc +9 -0
- package/.next/server/app/login/page.js +1 -0
- package/.next/server/app/login/page.js.nft.json +1 -0
- package/.next/server/app/login/page_client-reference-manifest.js +1 -0
- package/.next/server/app/login.html +1 -0
- package/.next/server/app/login.meta +5 -0
- package/.next/server/app/login.rsc +9 -0
- package/.next/server/app/opengraph-image.png/route.js +1 -0
- package/.next/server/app/opengraph-image.png/route.js.nft.json +1 -0
- package/.next/server/app/opengraph-image.png.body +0 -0
- package/.next/server/app/opengraph-image.png.meta +1 -0
- package/.next/server/app/page.js +24 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/twitter-image.png/route.js +1 -0
- package/.next/server/app/twitter-image.png/route.js.nft.json +1 -0
- package/.next/server/app/twitter-image.png.body +0 -0
- package/.next/server/app/twitter-image.png.meta +1 -0
- package/.next/server/app-paths-manifest.json +22 -0
- package/.next/server/chunks/247.js +12 -0
- package/.next/server/chunks/29.js +1 -0
- package/.next/server/chunks/343.js +1 -0
- package/.next/server/chunks/460.js +12 -0
- package/.next/server/chunks/533.js +38 -0
- package/.next/server/chunks/542.js +27 -0
- package/.next/server/chunks/590.js +6 -0
- package/.next/server/chunks/615.js +15 -0
- package/.next/server/chunks/696.js +25 -0
- package/.next/server/chunks/719.js +2 -0
- package/.next/server/chunks/739.js +1 -0
- package/.next/server/chunks/950.js +2 -0
- package/.next/server/chunks/font-manifest.json +1 -0
- package/.next/server/edge-runtime-webpack.js +2 -0
- package/.next/server/edge-runtime-webpack.js.map +1 -0
- package/.next/server/font-manifest.json +1 -0
- package/.next/server/functions-config-manifest.json +1 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +32 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/middleware.js +14 -0
- package/.next/server/middleware.js.map +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +1 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +1 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/149.a3e3a5dc03e21086.js +1 -0
- package/.next/static/chunks/2200cc46-7c93a0e00b0bb825.js +1 -0
- package/.next/static/chunks/788-aa413085174e935a.js +1 -0
- package/.next/static/chunks/945-3ff1d381a0af1ecd.js +2 -0
- package/.next/static/chunks/971-bb44d52bcd9ee2a9.js +1 -0
- package/.next/static/chunks/app/_not-found/page-200b7a7a6cfc29df.js +1 -0
- package/.next/static/chunks/app/api/docs/page-1dc18f40154cdce6.js +1 -0
- package/.next/static/chunks/app/layout-13e3cdaaa416edb6.js +1 -0
- package/.next/static/chunks/app/login/page-60d930d64f021753.js +1 -0
- package/.next/static/chunks/app/not-found-ae1139bed2018dd8.js +1 -0
- package/.next/static/chunks/app/page-583300dd8af66e5a.js +1 -0
- package/.next/static/chunks/framework-6e06c675866dc992.js +1 -0
- package/.next/static/chunks/main-app-8b0c4a1007dbb7f4.js +1 -0
- package/.next/static/chunks/main-e680fb049d7426e1.js +1 -0
- package/.next/static/chunks/pages/_app-0c3037849002a4aa.js +1 -0
- package/.next/static/chunks/pages/_error-a647cd2c75dc4dc7.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-117444a4bfe51057.js +1 -0
- package/.next/static/css/8c1b520a38ba4ccd.css +3 -0
- package/.next/static/vFM69sDrBUf_9ULwPmVAE/_buildManifest.js +1 -0
- package/.next/static/vFM69sDrBUf_9ULwPmVAE/_ssgManifest.js +1 -0
- package/README.md +389 -0
- package/app/api/auth/route.ts +103 -0
- package/app/api/beads/route.ts +27 -0
- package/app/api/beads/stream/route.ts +83 -0
- package/app/api/config/route.ts +48 -0
- package/app/api/docs/page.tsx +497 -0
- package/app/api/login/route.ts +42 -0
- package/app/api/logout/route.ts +14 -0
- package/app/api/oauth/callback/route.ts +97 -0
- package/app/api/oauth/client-metadata.json/route.ts +33 -0
- package/app/api/oauth/jwks.json/route.ts +32 -0
- package/app/api/records/route.ts +168 -0
- package/app/api/status/route.ts +25 -0
- package/app/api/v1/graph/route.ts +251 -0
- package/app/api/v1/issues/[id]/route.ts +158 -0
- package/app/api/v1/ready/route.ts +229 -0
- package/app/globals.css +230 -0
- package/app/layout.tsx +51 -0
- package/app/login/page.tsx +164 -0
- package/app/not-found.tsx +91 -0
- package/app/opengraph-image.png +0 -0
- package/app/page.tsx +2041 -0
- package/app/twitter-image.png +0 -0
- package/bin/heartbeads.mjs +225 -0
- package/components/ActivityItem.tsx +326 -0
- package/components/ActivityOverlay.tsx +125 -0
- package/components/ActivityPanel.tsx +345 -0
- package/components/AllCommentsPanel.tsx +270 -0
- package/components/AuthButton.tsx +202 -0
- package/components/BeadTooltip.tsx +246 -0
- package/components/BeadsGraph.tsx +2493 -0
- package/components/BeadsLogo.tsx +94 -0
- package/components/CommentTooltip.tsx +338 -0
- package/components/ContextMenu.tsx +272 -0
- package/components/DescriptionModal.tsx +595 -0
- package/components/GraphStats.tsx +121 -0
- package/components/HeartIcon.tsx +33 -0
- package/components/HelpPanel.tsx +339 -0
- package/components/MobileActionSheet.tsx +255 -0
- package/components/NodeDetail.tsx +793 -0
- package/components/SettingsModal.tsx +315 -0
- package/components/StatusLegend.tsx +99 -0
- package/components/TimelineBar.tsx +116 -0
- package/components/TutorialOverlay.tsx +235 -0
- package/hooks/useBeadsComments.ts +81 -0
- package/hooks/useIsMobile.ts +19 -0
- package/lib/activity.ts +377 -0
- package/lib/agent.ts +29 -0
- package/lib/api-helpers.ts +46 -0
- package/lib/auth/client.ts +221 -0
- package/lib/auth.tsx +159 -0
- package/lib/comments.ts +413 -0
- package/lib/diff-beads.ts +128 -0
- package/lib/discover.ts +228 -0
- package/lib/env.ts +33 -0
- package/lib/gate.ts +55 -0
- package/lib/parse-beads.ts +234 -0
- package/lib/session.ts +52 -0
- package/lib/settings.ts +42 -0
- package/lib/timeline.ts +138 -0
- package/lib/tts.ts +397 -0
- package/lib/types.ts +271 -0
- package/lib/utils.ts +48 -0
- package/lib/watch-beads.ts +97 -0
- package/next.config.mjs +4 -0
- package/package.json +81 -0
- package/postcss.config.mjs +9 -0
- package/public/image.png +0 -0
- package/scripts/generate-jwk.js +38 -0
- package/tailwind.config.ts +41 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { Metadata } from "next";
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
title: "Heartbeads API Docs",
|
|
5
|
+
description: "Public REST API documentation for Heartbeads",
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function ApiDocsPage() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="min-h-screen bg-white text-zinc-800 antialiased overflow-y-auto h-screen">
|
|
11
|
+
<div className="max-w-3xl mx-auto px-6 py-12">
|
|
12
|
+
{/* Header */}
|
|
13
|
+
<div className="mb-10">
|
|
14
|
+
<a
|
|
15
|
+
href="/"
|
|
16
|
+
className="text-xs text-zinc-400 hover:text-emerald-500 transition-colors"
|
|
17
|
+
>
|
|
18
|
+
← Back to graph
|
|
19
|
+
</a>
|
|
20
|
+
<h1 className="text-2xl font-semibold mt-3 text-zinc-900">
|
|
21
|
+
Heartbeads API
|
|
22
|
+
</h1>
|
|
23
|
+
<p className="text-sm text-zinc-500 mt-1.5 leading-relaxed">
|
|
24
|
+
Read-only REST API for AI agents, CI/CD bots, and integrations.
|
|
25
|
+
No authentication required. All endpoints return JSON with CORS
|
|
26
|
+
headers.
|
|
27
|
+
</p>
|
|
28
|
+
<div className="flex items-center gap-3 mt-3">
|
|
29
|
+
<span className="text-[11px] font-mono px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-600 border border-emerald-200">
|
|
30
|
+
v1
|
|
31
|
+
</span>
|
|
32
|
+
<span className="text-[11px] text-zinc-400">
|
|
33
|
+
Base path: <code className="font-mono">/api/v1</code>
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Table of contents */}
|
|
39
|
+
<nav className="mb-10 p-4 rounded-xl bg-zinc-50 border border-zinc-100">
|
|
40
|
+
<h2 className="text-[11px] font-semibold uppercase tracking-widest text-zinc-400 mb-2">
|
|
41
|
+
Endpoints
|
|
42
|
+
</h2>
|
|
43
|
+
<ul className="space-y-1.5 text-sm">
|
|
44
|
+
<li>
|
|
45
|
+
<a href="#graph" className="text-teal-600 hover:text-teal-700 underline-offset-2 hover:underline">
|
|
46
|
+
<code className="font-mono text-xs">GET /api/v1/graph</code>
|
|
47
|
+
<span className="text-zinc-400 ml-2">— Full project snapshot</span>
|
|
48
|
+
</a>
|
|
49
|
+
</li>
|
|
50
|
+
<li>
|
|
51
|
+
<a href="#issues" className="text-teal-600 hover:text-teal-700 underline-offset-2 hover:underline">
|
|
52
|
+
<code className="font-mono text-xs">GET /api/v1/issues/:id</code>
|
|
53
|
+
<span className="text-zinc-400 ml-2">— Single issue detail</span>
|
|
54
|
+
</a>
|
|
55
|
+
</li>
|
|
56
|
+
<li>
|
|
57
|
+
<a href="#ready" className="text-teal-600 hover:text-teal-700 underline-offset-2 hover:underline">
|
|
58
|
+
<code className="font-mono text-xs">GET /api/v1/ready</code>
|
|
59
|
+
<span className="text-zinc-400 ml-2">— Actionable issues</span>
|
|
60
|
+
</a>
|
|
61
|
+
</li>
|
|
62
|
+
</ul>
|
|
63
|
+
</nav>
|
|
64
|
+
|
|
65
|
+
{/* Common info */}
|
|
66
|
+
<section className="mb-10">
|
|
67
|
+
<h2 className="text-lg font-semibold text-zinc-900 mb-3">Common Details</h2>
|
|
68
|
+
<div className="space-y-3 text-sm text-zinc-600 leading-relaxed">
|
|
69
|
+
<p>
|
|
70
|
+
All responses include CORS headers (<code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">Access-Control-Allow-Origin: *</code>) and are cached for 30 seconds.
|
|
71
|
+
</p>
|
|
72
|
+
<p>
|
|
73
|
+
Every response includes a <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">_meta</code> object with <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">generated_at</code> (ISO 8601), <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">api_version</code>, <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">heartbeads_version</code>, and optional <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">warnings</code>.
|
|
74
|
+
</p>
|
|
75
|
+
<p>
|
|
76
|
+
Error responses return <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">{`{ "error": "...", "hint": "..." }`}</code> with appropriate HTTP status codes (404, 500).
|
|
77
|
+
</p>
|
|
78
|
+
</div>
|
|
79
|
+
</section>
|
|
80
|
+
|
|
81
|
+
{/* Authentication */}
|
|
82
|
+
<section className="mb-10">
|
|
83
|
+
<h2 className="text-lg font-semibold text-zinc-900 mb-3">Authentication</h2>
|
|
84
|
+
<div className="space-y-3 text-sm text-zinc-600 leading-relaxed">
|
|
85
|
+
<p>
|
|
86
|
+
By default, no authentication is required. When heartbeads is started with <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">--password</code>, all API requests must include the password:
|
|
87
|
+
</p>
|
|
88
|
+
<div className="space-y-2">
|
|
89
|
+
<div className="flex items-start gap-2">
|
|
90
|
+
<span className="shrink-0 text-[11px] font-mono px-1.5 py-0.5 rounded bg-zinc-100 text-zinc-600 mt-0.5">1</span>
|
|
91
|
+
<div>
|
|
92
|
+
<span className="font-medium text-zinc-700">Bearer token</span> (recommended)
|
|
93
|
+
<pre className="mt-1 bg-zinc-950 text-zinc-300 rounded-lg p-3 text-xs font-mono overflow-x-auto">
|
|
94
|
+
{`curl -H "Authorization: Bearer <password>" http://localhost:3000/api/v1/graph`}
|
|
95
|
+
</pre>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
<div className="flex items-start gap-2">
|
|
99
|
+
<span className="shrink-0 text-[11px] font-mono px-1.5 py-0.5 rounded bg-zinc-100 text-zinc-600 mt-0.5">2</span>
|
|
100
|
+
<div>
|
|
101
|
+
<span className="font-medium text-zinc-700">Query parameter</span>
|
|
102
|
+
<pre className="mt-1 bg-zinc-950 text-zinc-300 rounded-lg p-3 text-xs font-mono overflow-x-auto">
|
|
103
|
+
{`curl "http://localhost:3000/api/v1/graph?token=<password>"`}
|
|
104
|
+
</pre>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<p>
|
|
109
|
+
Unauthenticated API requests return <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">401</code> with a JSON error. Browser requests are redirected to a login page.
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
113
|
+
|
|
114
|
+
<hr className="border-zinc-100 mb-10" />
|
|
115
|
+
|
|
116
|
+
{/* GET /api/v1/graph */}
|
|
117
|
+
<section id="graph" className="mb-12 scroll-mt-8">
|
|
118
|
+
<div className="flex items-center gap-2 mb-3">
|
|
119
|
+
<span className="text-[11px] font-mono font-bold px-2 py-0.5 rounded bg-emerald-50 text-emerald-600 border border-emerald-200">
|
|
120
|
+
GET
|
|
121
|
+
</span>
|
|
122
|
+
<code className="text-sm font-mono font-semibold text-zinc-900">
|
|
123
|
+
/api/v1/graph
|
|
124
|
+
</code>
|
|
125
|
+
</div>
|
|
126
|
+
<p className="text-sm text-zinc-600 mb-4 leading-relaxed">
|
|
127
|
+
Returns the entire project state in a single response: all issues with comments and claims,
|
|
128
|
+
dependency edges, summary statistics, and a recent activity feed. This is the primary endpoint
|
|
129
|
+
for AI agents that need full project context.
|
|
130
|
+
</p>
|
|
131
|
+
|
|
132
|
+
<h3 className="text-xs font-semibold text-zinc-700 mb-2">Query Parameters</h3>
|
|
133
|
+
<div className="overflow-x-auto mb-4">
|
|
134
|
+
<table className="w-full text-sm border-collapse">
|
|
135
|
+
<thead>
|
|
136
|
+
<tr className="text-left text-xs text-zinc-500 border-b border-zinc-100">
|
|
137
|
+
<th className="py-2 pr-4 font-medium">Param</th>
|
|
138
|
+
<th className="py-2 pr-4 font-medium">Type</th>
|
|
139
|
+
<th className="py-2 pr-4 font-medium">Default</th>
|
|
140
|
+
<th className="py-2 font-medium">Description</th>
|
|
141
|
+
</tr>
|
|
142
|
+
</thead>
|
|
143
|
+
<tbody className="text-zinc-600">
|
|
144
|
+
<tr className="border-b border-zinc-50">
|
|
145
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">status</td>
|
|
146
|
+
<td className="py-2 pr-4 text-xs">string</td>
|
|
147
|
+
<td className="py-2 pr-4 text-xs text-zinc-400">all</td>
|
|
148
|
+
<td className="py-2 text-xs">Comma-separated status filter: <code className="bg-zinc-100 px-1 rounded">open,in_progress,blocked,deferred,closed</code></td>
|
|
149
|
+
</tr>
|
|
150
|
+
<tr className="border-b border-zinc-50">
|
|
151
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">priority</td>
|
|
152
|
+
<td className="py-2 pr-4 text-xs">string</td>
|
|
153
|
+
<td className="py-2 pr-4 text-xs text-zinc-400">all</td>
|
|
154
|
+
<td className="py-2 text-xs">Comma-separated priority filter: <code className="bg-zinc-100 px-1 rounded">0</code> (critical) to <code className="bg-zinc-100 px-1 rounded">4</code> (backlog)</td>
|
|
155
|
+
</tr>
|
|
156
|
+
<tr className="border-b border-zinc-50">
|
|
157
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">prefix</td>
|
|
158
|
+
<td className="py-2 pr-4 text-xs">string</td>
|
|
159
|
+
<td className="py-2 pr-4 text-xs text-zinc-400">all</td>
|
|
160
|
+
<td className="py-2 text-xs">Filter by repo prefix (e.g. <code className="bg-zinc-100 px-1 rounded">beads-map</code>)</td>
|
|
161
|
+
</tr>
|
|
162
|
+
<tr className="border-b border-zinc-50">
|
|
163
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">include</td>
|
|
164
|
+
<td className="py-2 pr-4 text-xs">string</td>
|
|
165
|
+
<td className="py-2 pr-4 text-xs text-zinc-400">comments,activity</td>
|
|
166
|
+
<td className="py-2 text-xs">Opt-in to expensive fields. Empty string skips both. Omitting returns all.</td>
|
|
167
|
+
</tr>
|
|
168
|
+
<tr>
|
|
169
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">limit</td>
|
|
170
|
+
<td className="py-2 pr-4 text-xs">number</td>
|
|
171
|
+
<td className="py-2 pr-4 text-xs text-zinc-400">50</td>
|
|
172
|
+
<td className="py-2 text-xs">Activity feed cap (max 200)</td>
|
|
173
|
+
</tr>
|
|
174
|
+
</tbody>
|
|
175
|
+
</table>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<h3 className="text-xs font-semibold text-zinc-700 mb-2">Example Request</h3>
|
|
179
|
+
<pre className="bg-zinc-950 text-zinc-300 rounded-lg p-4 text-xs font-mono overflow-x-auto mb-4">
|
|
180
|
+
{`curl http://localhost:3000/api/v1/graph?status=open,in_progress&limit=10`}
|
|
181
|
+
</pre>
|
|
182
|
+
|
|
183
|
+
<h3 className="text-xs font-semibold text-zinc-700 mb-2">Response Shape</h3>
|
|
184
|
+
<pre className="bg-zinc-950 text-zinc-300 rounded-lg p-4 text-xs font-mono overflow-x-auto">
|
|
185
|
+
{`{
|
|
186
|
+
"project": {
|
|
187
|
+
"name": "my-project",
|
|
188
|
+
"prefix": "my-proj",
|
|
189
|
+
"repos": [".", "../backend"],
|
|
190
|
+
"repoUrls": { "my-proj": "https://github.com/org/my-project" }
|
|
191
|
+
},
|
|
192
|
+
"issues": [
|
|
193
|
+
{
|
|
194
|
+
"id": "my-proj-abc",
|
|
195
|
+
"title": "Fix login redirect",
|
|
196
|
+
"description": "Full markdown description...",
|
|
197
|
+
"status": "open",
|
|
198
|
+
"priority": 1,
|
|
199
|
+
"issue_type": "bug",
|
|
200
|
+
"owner": "alice",
|
|
201
|
+
"assignee": "bob",
|
|
202
|
+
"labels": [],
|
|
203
|
+
"created_at": "2025-01-15T10:00:00Z",
|
|
204
|
+
"updated_at": "2025-01-16T14:30:00Z",
|
|
205
|
+
"closed_at": null,
|
|
206
|
+
"close_reason": null,
|
|
207
|
+
"prefix": "my-proj",
|
|
208
|
+
"blockers": ["my-proj-xyz"],
|
|
209
|
+
"dependents": ["my-proj-def"],
|
|
210
|
+
"comments": [
|
|
211
|
+
{
|
|
212
|
+
"author": { "handle": "alice.bsky.social", "did": "did:plc:..." },
|
|
213
|
+
"text": "This needs to be fixed before release",
|
|
214
|
+
"createdAt": "2025-01-16T14:30:00Z",
|
|
215
|
+
"likes": 2,
|
|
216
|
+
"replies": []
|
|
217
|
+
}
|
|
218
|
+
],
|
|
219
|
+
"claimed_by": {
|
|
220
|
+
"handle": "bob.bsky.social",
|
|
221
|
+
"did": "did:plc:...",
|
|
222
|
+
"claimed_at": "2025-01-16T15:00:00Z"
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
"dependencies": [
|
|
227
|
+
{ "from": "my-proj-xyz", "to": "my-proj-abc", "type": "blocks" }
|
|
228
|
+
],
|
|
229
|
+
"stats": {
|
|
230
|
+
"total": 42, "open": 15, "in_progress": 8,
|
|
231
|
+
"blocked": 3, "closed": 16, "actionable": 12
|
|
232
|
+
},
|
|
233
|
+
"activity": [
|
|
234
|
+
{
|
|
235
|
+
"type": "comment-added",
|
|
236
|
+
"time": "2025-01-16T14:30:00Z",
|
|
237
|
+
"issue_id": "my-proj-abc",
|
|
238
|
+
"issue_title": "Fix login redirect",
|
|
239
|
+
"actor": { "handle": "alice.bsky.social" },
|
|
240
|
+
"detail": "This needs to be fixed..."
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
"_meta": {
|
|
244
|
+
"generated_at": "2025-01-17T09:00:00Z",
|
|
245
|
+
"api_version": "v1",
|
|
246
|
+
"heartbeads_version": "0.3.7"
|
|
247
|
+
}
|
|
248
|
+
}`}
|
|
249
|
+
</pre>
|
|
250
|
+
</section>
|
|
251
|
+
|
|
252
|
+
<hr className="border-zinc-100 mb-10" />
|
|
253
|
+
|
|
254
|
+
{/* GET /api/v1/issues/:id */}
|
|
255
|
+
<section id="issues" className="mb-12 scroll-mt-8">
|
|
256
|
+
<div className="flex items-center gap-2 mb-3">
|
|
257
|
+
<span className="text-[11px] font-mono font-bold px-2 py-0.5 rounded bg-emerald-50 text-emerald-600 border border-emerald-200">
|
|
258
|
+
GET
|
|
259
|
+
</span>
|
|
260
|
+
<code className="text-sm font-mono font-semibold text-zinc-900">
|
|
261
|
+
/api/v1/issues/:id
|
|
262
|
+
</code>
|
|
263
|
+
</div>
|
|
264
|
+
<p className="text-sm text-zinc-600 mb-4 leading-relaxed">
|
|
265
|
+
Returns a single issue with its full description, threaded comments, claim info, and
|
|
266
|
+
enriched blockers/dependents (each includes <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">title</code> and <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">status</code> so
|
|
267
|
+
you can understand the blocking context without extra requests).
|
|
268
|
+
</p>
|
|
269
|
+
|
|
270
|
+
<h3 className="text-xs font-semibold text-zinc-700 mb-2">Path Parameters</h3>
|
|
271
|
+
<div className="overflow-x-auto mb-4">
|
|
272
|
+
<table className="w-full text-sm border-collapse">
|
|
273
|
+
<thead>
|
|
274
|
+
<tr className="text-left text-xs text-zinc-500 border-b border-zinc-100">
|
|
275
|
+
<th className="py-2 pr-4 font-medium">Param</th>
|
|
276
|
+
<th className="py-2 font-medium">Description</th>
|
|
277
|
+
</tr>
|
|
278
|
+
</thead>
|
|
279
|
+
<tbody className="text-zinc-600">
|
|
280
|
+
<tr>
|
|
281
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">id</td>
|
|
282
|
+
<td className="py-2 text-xs">The issue ID (e.g. <code className="bg-zinc-100 px-1 rounded">beads-map-abc</code>)</td>
|
|
283
|
+
</tr>
|
|
284
|
+
</tbody>
|
|
285
|
+
</table>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
<h3 className="text-xs font-semibold text-zinc-700 mb-2">Example Request</h3>
|
|
289
|
+
<pre className="bg-zinc-950 text-zinc-300 rounded-lg p-4 text-xs font-mono overflow-x-auto mb-4">
|
|
290
|
+
{`curl http://localhost:3000/api/v1/issues/beads-map-abc`}
|
|
291
|
+
</pre>
|
|
292
|
+
|
|
293
|
+
<h3 className="text-xs font-semibold text-zinc-700 mb-2">Response Shape</h3>
|
|
294
|
+
<pre className="bg-zinc-950 text-zinc-300 rounded-lg p-4 text-xs font-mono overflow-x-auto mb-4">
|
|
295
|
+
{`{
|
|
296
|
+
"issue": {
|
|
297
|
+
"id": "beads-map-abc",
|
|
298
|
+
"title": "Fix login redirect",
|
|
299
|
+
"description": "Full markdown description...",
|
|
300
|
+
"status": "open",
|
|
301
|
+
"priority": 1,
|
|
302
|
+
"issue_type": "bug",
|
|
303
|
+
"owner": "alice",
|
|
304
|
+
"assignee": null,
|
|
305
|
+
"labels": [],
|
|
306
|
+
"created_at": "2025-01-15T10:00:00Z",
|
|
307
|
+
"updated_at": "2025-01-16T14:30:00Z",
|
|
308
|
+
"closed_at": null,
|
|
309
|
+
"close_reason": null,
|
|
310
|
+
"prefix": "beads-map",
|
|
311
|
+
"blockers": [
|
|
312
|
+
{ "id": "beads-map-xyz", "title": "Deploy auth service", "status": "in_progress" }
|
|
313
|
+
],
|
|
314
|
+
"dependents": [
|
|
315
|
+
{ "id": "beads-map-def", "title": "Add SSO support", "status": "open" }
|
|
316
|
+
],
|
|
317
|
+
"comments": [...],
|
|
318
|
+
"claimed_by": null
|
|
319
|
+
},
|
|
320
|
+
"_meta": { ... }
|
|
321
|
+
}`}
|
|
322
|
+
</pre>
|
|
323
|
+
|
|
324
|
+
<h3 className="text-xs font-semibold text-zinc-700 mb-2">Error Response (404)</h3>
|
|
325
|
+
<pre className="bg-zinc-950 text-zinc-300 rounded-lg p-4 text-xs font-mono overflow-x-auto">
|
|
326
|
+
{`{
|
|
327
|
+
"error": "Issue not found",
|
|
328
|
+
"hint": "No issue with id \\"nonexistent\\". Use GET /api/v1/graph to list all issues."
|
|
329
|
+
}`}
|
|
330
|
+
</pre>
|
|
331
|
+
</section>
|
|
332
|
+
|
|
333
|
+
<hr className="border-zinc-100 mb-10" />
|
|
334
|
+
|
|
335
|
+
{/* GET /api/v1/ready */}
|
|
336
|
+
<section id="ready" className="mb-12 scroll-mt-8">
|
|
337
|
+
<div className="flex items-center gap-2 mb-3">
|
|
338
|
+
<span className="text-[11px] font-mono font-bold px-2 py-0.5 rounded bg-emerald-50 text-emerald-600 border border-emerald-200">
|
|
339
|
+
GET
|
|
340
|
+
</span>
|
|
341
|
+
<code className="text-sm font-mono font-semibold text-zinc-900">
|
|
342
|
+
/api/v1/ready
|
|
343
|
+
</code>
|
|
344
|
+
</div>
|
|
345
|
+
<p className="text-sm text-zinc-600 mb-4 leading-relaxed">
|
|
346
|
+
Returns issues that are ready to work on: status is <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">open</code> or <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">in_progress</code> and
|
|
347
|
+
all upstream blockers are <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">closed</code>. Sorted by priority
|
|
348
|
+
(critical first), then by age (oldest first).
|
|
349
|
+
</p>
|
|
350
|
+
<p className="text-sm text-zinc-500 mb-4 leading-relaxed">
|
|
351
|
+
This is the “what should I work on?” endpoint. A Playwright test bot
|
|
352
|
+
can hit <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">?type=feature&unclaimed=true</code> to find untested features.
|
|
353
|
+
A code reviewer can query <code className="text-xs font-mono bg-zinc-100 px-1.5 py-0.5 rounded">?type=bug</code> to find bugs needing review.
|
|
354
|
+
</p>
|
|
355
|
+
|
|
356
|
+
<h3 className="text-xs font-semibold text-zinc-700 mb-2">Query Parameters</h3>
|
|
357
|
+
<div className="overflow-x-auto mb-4">
|
|
358
|
+
<table className="w-full text-sm border-collapse">
|
|
359
|
+
<thead>
|
|
360
|
+
<tr className="text-left text-xs text-zinc-500 border-b border-zinc-100">
|
|
361
|
+
<th className="py-2 pr-4 font-medium">Param</th>
|
|
362
|
+
<th className="py-2 pr-4 font-medium">Type</th>
|
|
363
|
+
<th className="py-2 pr-4 font-medium">Default</th>
|
|
364
|
+
<th className="py-2 font-medium">Description</th>
|
|
365
|
+
</tr>
|
|
366
|
+
</thead>
|
|
367
|
+
<tbody className="text-zinc-600">
|
|
368
|
+
<tr className="border-b border-zinc-50">
|
|
369
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">unclaimed</td>
|
|
370
|
+
<td className="py-2 pr-4 text-xs">boolean</td>
|
|
371
|
+
<td className="py-2 pr-4 text-xs text-zinc-400">false</td>
|
|
372
|
+
<td className="py-2 text-xs">Only return issues with no claim comment</td>
|
|
373
|
+
</tr>
|
|
374
|
+
<tr className="border-b border-zinc-50">
|
|
375
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">type</td>
|
|
376
|
+
<td className="py-2 pr-4 text-xs">string</td>
|
|
377
|
+
<td className="py-2 pr-4 text-xs text-zinc-400">all</td>
|
|
378
|
+
<td className="py-2 text-xs">Comma-separated issue type filter: <code className="bg-zinc-100 px-1 rounded">task,bug,feature,chore,epic</code></td>
|
|
379
|
+
</tr>
|
|
380
|
+
<tr className="border-b border-zinc-50">
|
|
381
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">assignee</td>
|
|
382
|
+
<td className="py-2 pr-4 text-xs">string</td>
|
|
383
|
+
<td className="py-2 pr-4 text-xs text-zinc-400">all</td>
|
|
384
|
+
<td className="py-2 text-xs">Filter by assignee handle</td>
|
|
385
|
+
</tr>
|
|
386
|
+
<tr className="border-b border-zinc-50">
|
|
387
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">prefix</td>
|
|
388
|
+
<td className="py-2 pr-4 text-xs">string</td>
|
|
389
|
+
<td className="py-2 pr-4 text-xs text-zinc-400">all</td>
|
|
390
|
+
<td className="py-2 text-xs">Filter by repo prefix</td>
|
|
391
|
+
</tr>
|
|
392
|
+
<tr>
|
|
393
|
+
<td className="py-2 pr-4 font-mono text-xs text-teal-600">limit</td>
|
|
394
|
+
<td className="py-2 pr-4 text-xs">number</td>
|
|
395
|
+
<td className="py-2 pr-4 text-xs text-zinc-400">all</td>
|
|
396
|
+
<td className="py-2 text-xs">Max issues returned</td>
|
|
397
|
+
</tr>
|
|
398
|
+
</tbody>
|
|
399
|
+
</table>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<h3 className="text-xs font-semibold text-zinc-700 mb-2">Example Requests</h3>
|
|
403
|
+
<pre className="bg-zinc-950 text-zinc-300 rounded-lg p-4 text-xs font-mono overflow-x-auto mb-4">
|
|
404
|
+
{`# All actionable issues
|
|
405
|
+
curl http://localhost:3000/api/v1/ready
|
|
406
|
+
|
|
407
|
+
# Only unclaimed bugs
|
|
408
|
+
curl "http://localhost:3000/api/v1/ready?unclaimed=true&type=bug"
|
|
409
|
+
|
|
410
|
+
# Top 5 highest priority
|
|
411
|
+
curl "http://localhost:3000/api/v1/ready?limit=5"`}
|
|
412
|
+
</pre>
|
|
413
|
+
|
|
414
|
+
<h3 className="text-xs font-semibold text-zinc-700 mb-2">Response Shape</h3>
|
|
415
|
+
<pre className="bg-zinc-950 text-zinc-300 rounded-lg p-4 text-xs font-mono overflow-x-auto">
|
|
416
|
+
{`{
|
|
417
|
+
"issues": [
|
|
418
|
+
{
|
|
419
|
+
"id": "my-proj-abc",
|
|
420
|
+
"title": "Fix login redirect",
|
|
421
|
+
"status": "open",
|
|
422
|
+
"priority": 0,
|
|
423
|
+
"issue_type": "bug",
|
|
424
|
+
...
|
|
425
|
+
"comments": [...],
|
|
426
|
+
"claimed_by": null
|
|
427
|
+
}
|
|
428
|
+
],
|
|
429
|
+
"stats": {
|
|
430
|
+
"total_ready": 12,
|
|
431
|
+
"unclaimed": 8,
|
|
432
|
+
"by_priority": { "0": 2, "1": 5, "2": 3, "3": 2 },
|
|
433
|
+
"by_type": { "bug": 4, "feature": 6, "task": 2 }
|
|
434
|
+
},
|
|
435
|
+
"_meta": { ... }
|
|
436
|
+
}`}
|
|
437
|
+
</pre>
|
|
438
|
+
</section>
|
|
439
|
+
|
|
440
|
+
<hr className="border-zinc-100 mb-10" />
|
|
441
|
+
|
|
442
|
+
{/* Use cases */}
|
|
443
|
+
<section className="mb-12">
|
|
444
|
+
<h2 className="text-lg font-semibold text-zinc-900 mb-4">Example Use Cases</h2>
|
|
445
|
+
<div className="space-y-3">
|
|
446
|
+
{[
|
|
447
|
+
{
|
|
448
|
+
agent: "Playwright test bot",
|
|
449
|
+
endpoint: "GET /api/v1/ready?type=feature&unclaimed=true",
|
|
450
|
+
desc: "Finds untested features, writes E2E tests",
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
agent: "Code reviewer AI",
|
|
454
|
+
endpoint: "GET /api/v1/graph?status=in_progress",
|
|
455
|
+
desc: "Reviews work in progress, posts feedback as comments",
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
agent: "Planning agent",
|
|
459
|
+
endpoint: "GET /api/v1/graph",
|
|
460
|
+
desc: "Analyzes the full dependency graph, identifies bottlenecks",
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
agent: "CI/CD bot",
|
|
464
|
+
endpoint: "GET /api/v1/issues/:id",
|
|
465
|
+
desc: "Checks a specific issue's status after deployment",
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
agent: "Standup bot",
|
|
469
|
+
endpoint: "GET /api/v1/graph?include=activity&limit=20",
|
|
470
|
+
desc: "Summarizes the last 20 activities for daily standup",
|
|
471
|
+
},
|
|
472
|
+
].map((uc) => (
|
|
473
|
+
<div
|
|
474
|
+
key={uc.agent}
|
|
475
|
+
className="flex items-start gap-3 p-3 rounded-lg bg-zinc-50 border border-zinc-100"
|
|
476
|
+
>
|
|
477
|
+
<div className="shrink-0 w-5 h-5 rounded-full bg-teal-100 flex items-center justify-center mt-0.5">
|
|
478
|
+
<div className="w-1.5 h-1.5 rounded-full bg-teal-500" />
|
|
479
|
+
</div>
|
|
480
|
+
<div>
|
|
481
|
+
<div className="text-xs font-semibold text-zinc-700">{uc.agent}</div>
|
|
482
|
+
<code className="text-[11px] font-mono text-teal-600">{uc.endpoint}</code>
|
|
483
|
+
<div className="text-xs text-zinc-500 mt-0.5">{uc.desc}</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
))}
|
|
487
|
+
</div>
|
|
488
|
+
</section>
|
|
489
|
+
|
|
490
|
+
{/* Footer */}
|
|
491
|
+
<footer className="text-center text-xs text-zinc-300 pt-6 border-t border-zinc-100">
|
|
492
|
+
Heartbeads API v1
|
|
493
|
+
</footer>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getGlobalOAuthClient } from "@/lib/auth/client";
|
|
3
|
+
import { isValidHandle } from "@atproto/syntax";
|
|
4
|
+
import { OAuthResolverError } from "@atproto/oauth-client-node";
|
|
5
|
+
import { getRawSession } from "@/lib/session";
|
|
6
|
+
|
|
7
|
+
export const dynamic = "force-dynamic";
|
|
8
|
+
|
|
9
|
+
export async function POST(request: NextRequest) {
|
|
10
|
+
try {
|
|
11
|
+
const client = await getGlobalOAuthClient();
|
|
12
|
+
const body = await request.json();
|
|
13
|
+
const handle = body?.handle;
|
|
14
|
+
const returnTo = body?.returnTo;
|
|
15
|
+
|
|
16
|
+
if (typeof handle !== "string" || !isValidHandle(handle)) {
|
|
17
|
+
return NextResponse.json({ error: "Invalid handle" }, { status: 400 });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Store returnTo in session before redirecting
|
|
21
|
+
if (returnTo && typeof returnTo === "string") {
|
|
22
|
+
const session = await getRawSession();
|
|
23
|
+
session.returnTo = returnTo;
|
|
24
|
+
await session.save();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const url = await client.authorize(handle, {
|
|
28
|
+
scope: "atproto transition:generic",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return NextResponse.json({ redirectUrl: url.toString() });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error("OAuth authorize failed:", error);
|
|
34
|
+
let errorMessage = "Couldn't initiate login";
|
|
35
|
+
|
|
36
|
+
if (error instanceof OAuthResolverError) {
|
|
37
|
+
errorMessage = error.message;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { clearSession } from "@/lib/session";
|
|
3
|
+
|
|
4
|
+
export const dynamic = "force-dynamic";
|
|
5
|
+
|
|
6
|
+
export async function POST() {
|
|
7
|
+
try {
|
|
8
|
+
await clearSession();
|
|
9
|
+
return NextResponse.json({ success: true });
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error("Logout failed:", error);
|
|
12
|
+
return NextResponse.json({ error: "Logout failed" }, { status: 500 });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
import { Agent } from "@atproto/api";
|
|
3
|
+
import { getGlobalOAuthClient } from "@/lib/auth/client";
|
|
4
|
+
import { getRawSession } from "@/lib/session";
|
|
5
|
+
import { env } from "@/lib/env";
|
|
6
|
+
|
|
7
|
+
export const dynamic = "force-dynamic";
|
|
8
|
+
|
|
9
|
+
export async function GET(request: NextRequest) {
|
|
10
|
+
try {
|
|
11
|
+
const client = await getGlobalOAuthClient();
|
|
12
|
+
const url = new URL(request.url);
|
|
13
|
+
const params = new URLSearchParams(url.search);
|
|
14
|
+
|
|
15
|
+
// Retry OAuth callback up to 3 times for network errors
|
|
16
|
+
let oauthSession;
|
|
17
|
+
let lastError;
|
|
18
|
+
|
|
19
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
const result = await client.callback(params);
|
|
22
|
+
oauthSession = result.session;
|
|
23
|
+
break;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
lastError = error;
|
|
26
|
+
const errorMessage =
|
|
27
|
+
error instanceof Error ? error.message : String(error);
|
|
28
|
+
console.error(
|
|
29
|
+
`OAuth callback attempt ${attempt} failed:`,
|
|
30
|
+
errorMessage
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const isNetworkError =
|
|
34
|
+
errorMessage.includes("UND_ERR_SOCKET") ||
|
|
35
|
+
errorMessage.includes("fetch failed") ||
|
|
36
|
+
errorMessage.includes("Failed to resolve OAuth server metadata");
|
|
37
|
+
|
|
38
|
+
if (isNetworkError && attempt < 3) {
|
|
39
|
+
await new Promise((resolve) =>
|
|
40
|
+
setTimeout(resolve, attempt * 1000)
|
|
41
|
+
);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!oauthSession) {
|
|
50
|
+
throw lastError || new Error("Failed to create session after retries");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fetch profile information
|
|
54
|
+
let handle: string = oauthSession.did;
|
|
55
|
+
let displayName: string | undefined;
|
|
56
|
+
let avatar: string | undefined;
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const agent = new Agent(oauthSession);
|
|
60
|
+
const profile = await agent.getProfile({ actor: oauthSession.did });
|
|
61
|
+
|
|
62
|
+
if (profile.success) {
|
|
63
|
+
handle = profile.data.handle;
|
|
64
|
+
displayName = profile.data.displayName;
|
|
65
|
+
avatar = profile.data.avatar;
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.warn("Failed to fetch profile during login:", err);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Save user info to session cookie and read returnTo
|
|
72
|
+
const session = await getRawSession();
|
|
73
|
+
const returnTo = session.returnTo || "/";
|
|
74
|
+
session.did = oauthSession.did;
|
|
75
|
+
session.handle = handle;
|
|
76
|
+
session.displayName = displayName;
|
|
77
|
+
session.avatar = avatar;
|
|
78
|
+
session.returnTo = undefined; // Clear after use
|
|
79
|
+
await session.save();
|
|
80
|
+
|
|
81
|
+
// Redirect to the page the user was on before login.
|
|
82
|
+
// Use PUBLIC_URL when behind a reverse proxy (Caddy/nginx), since
|
|
83
|
+
// request.url will show the internal origin (e.g., http://localhost:3000)
|
|
84
|
+
// instead of the external one (e.g., https://heartbeads.duckdns.org).
|
|
85
|
+
const origin = env.PUBLIC_URL || new URL(request.url).origin;
|
|
86
|
+
const redirectPath = returnTo.startsWith("/") ? returnTo : "/";
|
|
87
|
+
|
|
88
|
+
return Response.redirect(`${origin}${redirectPath}`, 303);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("OAuth callback failed:", error);
|
|
91
|
+
const origin = env.PUBLIC_URL || new URL(request.url).origin;
|
|
92
|
+
return Response.redirect(
|
|
93
|
+
`${origin}/?error=${encodeURIComponent("Authentication failed - please try again")}`,
|
|
94
|
+
303
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|