mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-09 03:17:58 +00:00
Add support for cloud job auth tokens (#182)
This commit is contained in:
parent
0cbd4caec2
commit
e20feaa1ab
29 changed files with 1662 additions and 81 deletions
|
|
@ -38,6 +38,7 @@ COPY packages/config-typescript/package.json ./packages/config-typescript/
|
|||
COPY packages/env/package.json ./packages/env/
|
||||
COPY packages/db/package.json ./packages/db/
|
||||
COPY packages/ipc/package.json ./packages/ipc/
|
||||
COPY packages/job-auth/package.json ./packages/job-auth/
|
||||
COPY apps/roomote/package.json ./apps/roomote/
|
||||
|
||||
RUN pnpm install
|
||||
|
|
@ -47,6 +48,7 @@ COPY packages/config-typescript ./packages/config-typescript/
|
|||
COPY packages/env ./packages/env/
|
||||
COPY packages/db ./packages/db/
|
||||
COPY packages/ipc ./packages/ipc/
|
||||
COPY packages/job-auth ./packages/job-auth/
|
||||
COPY apps/roomote ./apps/roomote/
|
||||
|
||||
WORKDIR /roo/apps/roomote
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ COPY packages/config-typescript/package.json ./packages/config-typescript/
|
|||
COPY packages/env/package.json ./packages/env/
|
||||
COPY packages/db/package.json ./packages/db/
|
||||
COPY packages/ipc/package.json ./packages/ipc/
|
||||
COPY packages/job-auth/package.json ./packages/job-auth/
|
||||
COPY apps/roomote/package.json ./apps/roomote/
|
||||
|
||||
RUN pnpm install
|
||||
|
|
@ -52,6 +53,7 @@ COPY packages/config-typescript ./packages/config-typescript/
|
|||
COPY packages/env ./packages/env/
|
||||
COPY packages/db ./packages/db/
|
||||
COPY packages/ipc ./packages/ipc/
|
||||
COPY packages/job-auth ./packages/job-auth/
|
||||
COPY apps/roomote ./apps/roomote/
|
||||
|
||||
WORKDIR /roo/apps/roomote
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ COPY packages/config-typescript/package.json ./packages/config-typescript/
|
|||
COPY packages/env/package.json ./packages/env/
|
||||
COPY packages/db/package.json ./packages/db/
|
||||
COPY packages/ipc/package.json ./packages/ipc/
|
||||
COPY packages/job-auth/package.json ./packages/job-auth/
|
||||
COPY apps/roomote/package.json ./apps/roomote/
|
||||
|
||||
RUN pnpm install
|
||||
|
|
@ -101,6 +102,7 @@ COPY packages/config-typescript ./packages/config-typescript/
|
|||
COPY packages/env ./packages/env/
|
||||
COPY packages/db ./packages/db/
|
||||
COPY packages/ipc ./packages/ipc/
|
||||
COPY packages/job-auth ./packages/job-auth/
|
||||
COPY apps/roomote ./apps/roomote/
|
||||
|
||||
WORKDIR /roo/apps/roomote
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ OPENROUTER_API_KEY=encrypted:BA3bYeSEXFA0jnw3lmzLKz53ekkGr0K6sDLN6+nFGsa0TdJrYoK
|
|||
|
||||
SLACK_API_TOKEN=encrypted:BJyTFw3gugKZr6uaNZaqCH0UpW/mP4S8P1oL5NzeCvoZzSMJEcDrKsW+b+3Ojp0EuqBFI7T8SPUfRMr6ZTSI880+pKYWkmfAAl2jYYE5aw48LGkg+WJTjIW8g2jTEc3PXPYeMzszrJ2Igf8NuyDMLmhI9S9ew5OfhxD6bKwr73WdUKVDJ+MMVBQGIkbrt1PBX30kRuKZq5oqiA==
|
||||
|
||||
|
||||
|
||||
FLY_ACCESS_TOKEN="encrypted:BDm2qpf85nXBtq7KENGMiwcsFg3Ln/gRcZoUm5busQ8tRgEDXhlXUB4gSUT+Z/XRvwurBcBmYZhkjkDycNQLME53htE8lMLfBqmPTIWFgHhKVZWtlk3EhC7GZcycuLYKZVFuEsvKxu4kH5JibqUtJ5fRjKEbQG1HXuTvwuwGjSahgZC3I+Q1xb/83+nFmHKAr0ibaLHHO/mbZZ4YLSS91dw/BiVPmhaY2eDpdDvrLkYZVJZ28f+JkYocYyNUdaCnUqlzfpQAa6xBTelk9OaNwm3XnCYlm+VssMQpN6z5iOyBDG4Jr2Wx8eNyg9PC5lOptweYRYh2ho3RYW0gDwYujfFfRpMKGaEnCB2l/++iDWB3LY9p3xh8ysJ59OTMb8+luOHJbZdiwbnOgY4WcaF0nJyPNX8aAq22Z/imvFqb5hFnytg5kD2mn4h6KG36NmldAw/zJeyat8j4KQEZSW9pBv/vXN2UtTWnA+vsgnua5SHaHoa2GHN/8HyODbdNPWv1tWnjOhZGbxa1+MzkX/4w6BS9jNG+6Pe4m0YVZMjX/Bv6B8qIrcKqVouBrsKIjN9BSFdQzqlDLk6GsP25Nu/eTQS8+/fuSbI+mn7eo84xHWtawlgej4ydkNDOBVmUV9xgoST8LGq6H6Gq5zvuNyY8IStglpybN1yQzEP2RT+JNM112qQK5EWuWTEGuMq4C8ZIIO8ETgDgDjVBGuEeOVFIHCpXzrwB9HTWf3W4DO7JKeNRBmbYkNBTbr3AqzqRKa/+aTw4G8YTyn0T++AaYKUv9KZAajtZ6TQKL1bkGOvsqBBzRW1/NCyuRxEt7+r/ffx0zo7qLW6ir2gTZuDoPi8kQIu8RxkGJypg8QYZHGtr3yGXxm8IEjPlCYINx2RWZjFr0RDO6FRqlexCnnnj10n+TZJgeEQb0esGXJd20klhYwngi9BnvIWoOPLPPeC+JHeILVAUgAxKjsspF5ck3I/Td2u/"
|
||||
|
||||
JOB_AUTH_PRIVATE_KEY="encrypted:BM7xBs73k3nh0isBhVELr4LFt2iMiP7XD7knKtEnm7uudQ9DclnL+Hulun8SFFsGzkIp+FyYK47JAMHqyK3NqGHE/93efpACNRNH/YVv0OfUvVw58trPdvdY60hnUrW17z/ae16LlvkBd6roCZxn0GvhgMKPRxlL1B4ZxxtfHfz+yazl0tSsr2t/iMzIhRCtQZ1638PrZJ7ugmsK1lK5g+cQDV91Onth+eVNy6cDX9NAC77iotnzXyOWPyzcxECCSje2jYysAugTTRV2pR/Lnzi506yrWumSv5SpNcDqZjek89CK29nfUJv+UIxxW2dIMIB19ZqjmSU84nIYymVzkwl7kOaDqTIEIMRSzYs/npTWDOGbtB2x49vaD5YeysFiamLlaSzvS46N+uiSrH+8VCujs7HLirOJhtkindVRiV4qfl/eycf9OG2A4jNWhS/uJYcF2QC8KZL1SwKcn7u+FDov1NAYUw25BmeYVvb+KPujsaH1DniqucaxtcGqior1ZjKU5RutbzV5QWknpNAtBD2aVv0HOB7/5bHJpwFl2vaR"
|
||||
JOB_AUTH_PUBLIC_KEY="encrypted:BOU3vH2HFJjRuORyr3psrDpqaXS1hXo2SxA3N2q4M2ihHgHNnbjbKAV3xYHTo9WQA6XGvRWXpo3C/soV6ukMQGkqmvjyCNPVLN3ZlJJlmbBC1qRTn83dejp4YCqV0eb3JOyOhYVpuCCIcYU3s67MRqywTxST1h1PRo3xykpgm3nY0kfZMmHvL7rTh4ik6uVebDhIY7jDrXRWlj/dia/cyEDzB88cBpmCpJx8uqFmK9889+Le4PTb7hLBIv7KvkzMb4TaB4yqpUFd/bawA0fOX5WC85t2DZ+7+Ddqp7s4x6nsNWjh51t0uYGICdF1ReWI1jZ4nKGjxpXcjfmgcXPnOT6PdjOWfMWfMGq+sE7o1GUdu5GKVwATYe4+NXrcMVsTQKdbtRrl4k1lz46jgAegUemGuBxR4Ro6qxK5xhsKeyaMnbrH8f/X0BZHAGOx"
|
||||
|
|
|
|||
|
|
@ -21,3 +21,6 @@ OPENROUTER_API_KEY=encrypted:BDoXCMz3fV5DXa89cwd77/pZhw6tVIw96LvtyZEFwFQtQV1Qk+Z
|
|||
SLACK_API_TOKEN=encrypted:BEmAqaFftmVNlZdx19KGXBDtJ1E1SFBJrAH52pwhFR00Eq6DJAOJSBLY+9lV4wsDRnhTaR8QBQ5fx4kX6751mqJ/Ldu9n7zYV+NTV5cWowvTh4jD+w7yufbUvNgnqs1CpvGXysaJoyQCQqQlUJ+wxlf+rb3yEN5CMR9y1Es/d+cXF2d73H2VfBDtVcKf1pCPn+kVeQW9XHquiw==
|
||||
|
||||
FLY_ACCESS_TOKEN="encrypted:BCoSnp5DqB0Td27HYtDwIS/dZfZkyvjsgUz+0XcKdovFMr3/ZRZkjLW8GgBpNIdvxYz+/iS8wGPtnFHmD73Kf1+CU5eKNWUTwRaxoD7GyCTXvWwYnNt55unoNpr51rCWTgQ66M8aQGm85gC+948woiL64i5ozwWezkxbPtk4vOM5dbbeIObzfPoPKOqFDOIdFjfbaEFpd6Yuw2BrxCT9HExiK+IOp2xUNEvB4/w4xbzenDDkkyw+kNx0hcFvkefX2EkuSHRS3nPVKMFVH2Pcfc7HuccAbmjIvlvuKMulyiAyJi7XrkyW46ZN7BCRokYR0j7zIqAhQmNgYgs37tbiq+CvqDHbkOEqGIZZIxV+pzONbJ07moAbDBpBeMJ6WeH6gAURJ3mqa3tCyCEFk8TxHMe5lbfjJGuYe4SakrOTMPOMH451gtw5KdUN6m7XFKfL1+MbklpIy/lVgOWAKiyMlxxUmeDShn4ElMIKEGYxY8gOcI5Akyn/3B3tk1RgutTQ7p3gjya5GCf2hKNnOAAjlRzOfDbnObslQZv/ZG1QQ81VLqpvj0S2zMDITwjuGCey6MT9UXrVs1JYaLPsJHiEKlylWuXRenWBaVz2iRdFugRhx1PbisOPkgrqhU5K/B5d8vpsuKOCX65ty8CkJq3KoW6TzzkO2nAm8LJ10Ihv3y+fGRCgUUmjLmSOzg7yG6a89wklCUW3BS3bDdvgEfBMGcdPH/9BTVOQ5wpXZWB9CX4tvPLlXnLyWqH6VWShsfhRRL0MoQ9PXMD9NK61jHIw8RXB1xAASL8B+NeVthj490ymhUHuaL0nBetS38W+tD6jE5IVAyvaR4tvz/4eJz/uxbvgqCJyd4GFW8kyp6blTziksryr4TlLzS7d1F84WKIjUcNyaG5te/L4GixvnPQ5nxFo2Ib464knuljvjdDRxLiOk4H8k7S45OLx2seSsV+KTGlKeN3S013rpXFnG5APUEvz"
|
||||
|
||||
JOB_AUTH_PRIVATE_KEY="encrypted:BFm76/By4KHU3bILl4tYJOwwHN8xR49C/wcSrh8/0+QAhYgetocntzJ90fYS3+ER2e/Mdh1awOynhbsZ/gpLJC6PmqX+MKzkpv/xJC5AJZTAeyWAKwaL1NhUijAU8mS88/87EvKZCH9Rw/tBGy/7CFrOwipjKQxvuRZjgxmypkV8t6eIEmfGgIbzrjvSyZ6O31FpYliAABeefRpNQo3zd7tV3dX9xtVDt1Hc68vzak0Aw+EJwi+Q7YWvghpcLJxXXo+PYs15Cj9LS8Ww2Ijz/EXO98UIDMN1e947O7hrmTWPC/JVZlGJOctMyNyaX/+rSJP+XepJxkVYeG9jglOLOsIFS+KKiYfnlT3TC5pt9USaAQAsdWvcfT09TN4qYR/eeSfj7mZYNCU4LtIqrFVT65ehntTOokqN7uNSLmSQnzPIO9lhIVcarFz1C9Bx/xE4FaBVKm9agszVcUumJcfktuV/WTxRxgTmhJYbJ08shhrTubWDCxOGdr5NFR+NFN5oMe9KXCOcUjtuHQ8UUSjYwXvZzJOvRAMP0QAv5xzGTlQI"
|
||||
JOB_AUTH_PUBLIC_KEY="encrypted:BPcQ3GgxlCm77QmmsLm5jzFLzCVRuDJzPC7t3jfMbPYNGMBveYIJvrmnpHCNSJIGCrHi0MtFUypvyjjHgQADNN8GaWpyvifk8jTBtp1ptQq736ilxq/Z76KY96uHf59TQCvPR2pjsu7GK0AqL3wg1MV+30+wZzGNo2JYZYczMdYgrMj72d2qsOPydUgPAkXcwRMigNyPhF8JdOT9G5l1w+qRKjRSFynFIXcP61J9kykeYz3BmCIeKw2QopSYz3ie98o+PTmnyQ3KGgEdJ7IVTmV3AglWMJx6Gj6qo8hMW+pUJU790nj9FMkMHHnAIRvhEX9MmInNu+ysVuowKWfxB3IC8A9ktgk3TOZ4/Ub1EZ3rWGVyR9xVFLSYPUlW2s/PfPA8b+KdSd/QDRpGhJ0rd6pl8r33/HOErwu6Mta/4NHIBR9SRj8SrjCNTPG0"
|
||||
|
|
|
|||
|
|
@ -21,3 +21,6 @@ OPENROUTER_API_KEY=encrypted:BLBANC6AI/i5gHIeiBaY5lOogEEahxSLAvsxUpk6Z1FXtfrIEj/
|
|||
SLACK_API_TOKEN=encrypted:BHQC5Lv0k3VnZ0Ez4TcVvL0meJs8R5ysPFa47U3kkjOJstrtDa26F8Fjvd8/Qr7ztiKdJzGXpr9k4/Tm8mL9jZvG7HNH9D+fVHkyGZ2Mf7B97FdXjiDxA3f+Pifj0aTfM7U58tDrKTLeQ/6UzE3Emcxyg4CxKjAjEaRFOAiQxs1lVAtqzbeF7OCV6GHScpSe4zoAaFXvtQ07FA==
|
||||
|
||||
FLY_ACCESS_TOKEN="encrypted:BFcS7pt6Evj8OG1khLFJkqzg2MI/pP4g4tP1l85vbsiTL1x1OvjsIqhZvGUm/PkamInYMTVOhWOWHx/s0Iv3F++FfXeWCzRTaaGvCupMgGB5BEEckODT+4GLrsc14NUCT6p+DE/iREzkJFlHjPwj8NergqnUwWmxhNyFDxFK0AZKpZAKSf69/WrAHDN5I907mOm4wUkPag03L9SuhaotCedsv/zqvIfvbr8gxyk7N1vphXaiihVtE+GIaBnHkHjz3eiS/kDtKFPPeZT1lS9yE9T00Bah2hZkSIxpIu4Cd3zAAWYIplqlhDrO32tm89gPSeQfn7ORaHPogK5I8n9ctlKJV8PFO9YoYqaq8TUnkDzvLj7BNk7GpPibodl2mSX8eVspJXCSewKhnbVFA1RNWJ6B3Bqp5n3PmmBaF4n0VCYMzxUHT4UvOfoQGH1GhzVQiQkV0+s8jfD3eRFiovpvOc89WXmuhtGGMwZ0pYrFWg1qTgmUndcfKzob7yzvrlLpJ+kqp6X0c+cO26yOfN+7LfnZt9BeuzzHSEN9+aEWfHv8IK/xqDjT/V8ZoctOGJG/nFFqF7OeYk2Er0AvD+53kwXBCKiBRjx/b2w6s+Z4f71+62alH/Pm62Q3vWGpXIuj8GF7CaHIQD9FH6cegOa7klabrdXjyIETQLPtpKniwVJbaRhEj9p+WOB0dhibZVFX/LtQ8hIt1HuV0TcoB1jLToN835gPuPV5db+Zs0qf3v0lgM4dRM2N3R7d+XdTVZVvfpdJTTNpN9P6AMrHjTHGp2sCDkInymjrhZlpwD3h3XcsnbcGkjTraJsoNsfGGoH1JUyX0AVyF4ec61eFiaFec4RGH2Oxn/rdCq/X4xbqM7ZQhxhhBhkyIMlztRONXYfz8Jqn+f9KK4lxuhg5Ao/xWWyqv5UCeGh2KB32CO1ez3ei9QqD72wyw/NJLLh8XPW1f9TPuFQsfQTG1ftljC2w/XGu"
|
||||
|
||||
JOB_AUTH_PRIVATE_KEY="encrypted:BNv2gXyx2FICPfSCJxDJX0ELwt35/Ie/WR0xUai/k9OqEM8p2rhLkRkde45dioGlEPbETZgWa6PDD9mXCR8qBWh4iZziDEwYd8x1uFTx26BBs1MyWk8WhRhjhQMXi0h8V7inVELiPtMcq40aSJ/2S2yTR/qyNlW0YrhQJO1FeU13n5Eelp5PSvmCVDMu+f5m4m5haiJIzvnXbnETOGqatWWNn3qXY/neyfTLlv21E95ulu2bUP1/4iRw4tf0e5LIf4TLeo3M2tSuUc/s7sELb6hNkBtBZCPkQG1+dHci/6MLrer0LiMF3Pv3fUP9ZRfRu8AnOjHuk6xBq3EshbTE+NQoWSf/47F23lHnY+pU+1cT2W+3L21RvZiVoENWC00/udbyNx5GglZmRY8+pZHUwS0l7FFVFtjXJxQigLtMsCijmmZhfz+JAlOouOUoh26HzvB7dcyHnwsXhGwEiG8wmUi8lFCmFosdMp210SRpfrJ/NywObBHYSUXDyELEpS6+GTeZQDmUs44vQ7v58waDHRQvJXGxIw4zK+s+WGlbHTnF"
|
||||
JOB_AUTH_PUBLIC_KEY="encrypted:BAWhQhxInT33caEIc2QQ16wSjJ7SqJ2nYc0FEwQboBfajqh9mp2SVwH/A9CbbM+JrVbqYjNamVCf+BdIJVKn4rTrrV64HVMI5T++EfxPmN59sQWgqb4Ig3DXnoo9UanXGccn23dB/k4ZtweoHSPeVXn8XbZ867TPO3QLI1Xr6fLfgxHZXcyvH6lEZlClJjM+SLRLtixkvKb5c2y1JG6cVQ+dHrvxuh10FdCFzfhkEvPHdv09dWpw6OrPfxBEk+dHpjXWO54p/4WThjJ+41Rpqne0pupLwkILaa/xMj364Q/duTKORUDdnDc+Mk9TMgHuj2M5kQGSSXXuNO4shGxupoA2y5HRI9IhjNKQIq2qikUjzzxjvfHs5ESj6DfJsDq5AwiqU6vD7XcQFbJLaUZc8FioN+gEiHnNvCoi6ZSKjvrCggt5afN+lGVNyROe"
|
||||
|
|
|
|||
|
|
@ -20,3 +20,6 @@ OPENROUTER_API_KEY=encrypted:BJHyYNaUsHiU6By54yUA2lG48W6NFuafGOr6Sc4XDopmTB9WulF
|
|||
|
||||
SLACK_API_TOKEN=encrypted:BDkskSYEb9SYQLpS0LyyOMgS7pVOyha9agMrS15iQUBoeULTzXbfGxEc+w3oEUa7mFyuRW+gHpo1jqEYicc2Nm9xXawDFsfhoTgXkhUdDlAVsJY/jctWtw97Hb9qLvigRrJWyx7ha5wUMg==
|
||||
FLY_ACCESS_TOKEN="encrypted:BIobJarcf4MpmWDkSjVGjXUH87dNj5rbMqow0/7QNpa1s3eMZeRNoi8oMGn3lBSgPv/hZjCUJJ9t8+5mZNgZk0CrrLUz9W1VxD1hIJwzYlKA6vhR8Ef+A8z6W4H+XBl3zFXPxfvfdy+v"
|
||||
|
||||
JOB_AUTH_PRIVATE_KEY="encrypted:BB5ohcsYq19krr7Rw4CVtS6GcxEbpWPeobkByt0ppr6oU92AgbbdgMdEEyA8IRFpK6H87ud6DdfV4iRnkwART3HhdJJZe06R8MfV6SCxUioz6uwRmagmeJ5/cmnViUihhFiq/NC7qSD/zfRREuhyKXA/Tr1KTllaoQn89PzpfVM42duSRZcSRQ3/vHV62v+MIRFvm2gWqL2mwgr7RtRDpijyzFvSzgerrorHMOPQTYL5uYAcS20lnFEnWzJfDU/hZmqFvyCnxG3u80IlPu+RWN7xLs/WtXZr+5UFP8sgVFRi18+xrNtam3auA163HbC8u0mk3MeYHfKihN35j+2U24oEg+sflFPSm1EfXS+da1wd7GsVfOv/9jRhSrXju1Vg1DI3PSBygiQXpTD23KtOFy5v96RtAGGUeq94Fxti1jTzngaUNxBSCO69L7PL2fo2g2ZML2GlSfSq7R8Q/T7zsjpirbecOLermJnMP0hgrkCTDaqD008iXYte01wVOqgNRuXIXtIqGrsUSf+eGLLP669TqIbNPOSq1tccOtNnPVAH"
|
||||
JOB_AUTH_PUBLIC_KEY="encrypted:BI7/E85qpiZMVZ79gLNRMETFCBoja4wq1Gw03BAWTct3GoCypOsgm2tYvbPOVpe/XKqFot7WVzGhaWrvx9blzrp1+LPAHcChNoa61CeUMtrNAL5VQETZ/9SJU1zLFWpHlCJuviu8G+DlC0UwfAQBdutcwhlcwP8fQSGGXgSaCcnshlhsX/tuqSnyCOrQ2kERXEFK4QT0ngETFdOzVqO0/QHbk6tL/86iv4XQ7Iz2g9XLCdAWXxcLzRn7RE9BXna4eCPahJI8NZx50E3gtD6IsQj7msL1p20PWauOJsm4futPdPNZe2k8plR48PAxTYRHgRHwHJukEqQcNjL5LdXBW0fMNQRBJLYjgxJfAs0cm6ioYXqj0cliDharBGQ71YMjsihsCPZAsozibddiKT02lEtLQOiHOp27wfDNkSOXvspkaaxjHoB4j6cTSQWb"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"@roo-code-cloud/db": "workspace:^",
|
||||
"@roo-code-cloud/env": "workspace:^",
|
||||
"@roo-code-cloud/ipc": "workspace:^",
|
||||
"@roo-code-cloud/job-auth": "workspace:^",
|
||||
"@roo-code/types": "^1.30.0",
|
||||
"bullmq": "^5.37.0",
|
||||
"cmd-ts": "^0.13.0",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import {
|
|||
EVALS_SETTINGS,
|
||||
} from '@roo-code/types';
|
||||
import { IpcClient } from '@roo-code-cloud/ipc';
|
||||
import { createJobToken } from '@roo-code-cloud/job-auth';
|
||||
import { db, users } from '@roo-code-cloud/db/server';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import type { JobPayload, JobType } from '@roo-code-cloud/db';
|
||||
|
||||
|
|
@ -53,6 +56,8 @@ export type RunTaskCallbacks = {
|
|||
type RunTaskOptions<T extends JobType> = {
|
||||
jobType: T;
|
||||
jobPayload: JobPayload<T>;
|
||||
jobId?: number;
|
||||
userId?: string;
|
||||
prompt: string;
|
||||
logger?: Logger;
|
||||
callbacks?: RunTaskCallbacks;
|
||||
|
|
@ -64,6 +69,8 @@ type RunTaskOptions<T extends JobType> = {
|
|||
export const runTask = async <T extends JobType>({
|
||||
jobType,
|
||||
jobPayload,
|
||||
jobId,
|
||||
userId,
|
||||
prompt,
|
||||
logger,
|
||||
callbacks,
|
||||
|
|
@ -80,9 +87,36 @@ export const runTask = async <T extends JobType>({
|
|||
const cancelSignal = controller.signal;
|
||||
const containerized = isFlyMachine() || isDockerContainer();
|
||||
|
||||
let envVars = `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath}`;
|
||||
|
||||
// Create JWT token if we have jobId and userId
|
||||
if (jobId && userId) {
|
||||
try {
|
||||
// Get user's org info
|
||||
const user = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1);
|
||||
const orgId = user[0]?.orgId || null;
|
||||
|
||||
const token = await createJobToken(
|
||||
jobId.toString(),
|
||||
userId,
|
||||
orgId,
|
||||
TIMEOUT,
|
||||
);
|
||||
|
||||
envVars += ` ROO_CODE_CLOUD_TOKEN=${token}`;
|
||||
} catch (error) {
|
||||
logger?.error('Failed to create job token:', error);
|
||||
// Continue without token - job will fall back to no auth
|
||||
}
|
||||
}
|
||||
|
||||
const codeCommand = containerized
|
||||
? `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath} xvfb-run --auto-servernum --server-num=1 code --wait --log trace --disable-workspace-trust --disable-gpu --disable-lcd-text --no-sandbox --user-data-dir /roo/.vscode --password-store="basic" -n ${workspacePath}`
|
||||
: `ROO_CODE_IPC_SOCKET_PATH=${ipcSocketPath} code --disable-workspace-trust -n ${workspacePath}`;
|
||||
? `${envVars} xvfb-run --auto-servernum --server-num=1 code --wait --log trace --disable-workspace-trust --disable-gpu --disable-lcd-text --no-sandbox --user-data-dir /roo/.vscode --password-store="basic" -n ${workspacePath}`
|
||||
: `${envVars} code --disable-workspace-trust -n ${workspacePath}`;
|
||||
|
||||
if (!logger) {
|
||||
logger = new Logger({
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@roo-code-cloud/db": "workspace:^",
|
||||
"@roo-code-cloud/env": "workspace:^",
|
||||
"@roo-code-cloud/job-auth": "workspace:^",
|
||||
"@roo-code/types": "^1.30.0",
|
||||
"@sentry/nextjs": "^9.23.0",
|
||||
"@tailwindcss/postcss": "^4.1.8",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Env } from '@roo-code-cloud/env';
|
|||
|
||||
import { type AuthResult, type ApiAuthResult, isOrgRole } from '@/types';
|
||||
import { logger } from '@/lib/server';
|
||||
import { validateJobToken } from '@roo-code-cloud/job-auth';
|
||||
// import {
|
||||
// type AgentTokenPayload,
|
||||
// validateAgentToken,
|
||||
|
|
@ -44,64 +45,39 @@ export async function authorize(): Promise<AuthResult> {
|
|||
* Validates authentication and authorization for API endpoints.
|
||||
*/
|
||||
export async function authorizeApi(
|
||||
_request: NextRequest,
|
||||
request: NextRequest,
|
||||
): Promise<ApiAuthResult> {
|
||||
return authorize();
|
||||
const authHeader = request.headers.get('authorization');
|
||||
|
||||
// const isAgent = request.headers.get('authorization')?.startsWith('Bearer ');
|
||||
if (!authHeader?.startsWith('Bearer ')) {
|
||||
return authorize(); // Fall back to Clerk auth
|
||||
}
|
||||
|
||||
// if (!isAgent) {
|
||||
// return authorize();
|
||||
// }
|
||||
const token = authHeader.slice(7);
|
||||
if (!token) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Unauthorized: Malformed authorization header',
|
||||
};
|
||||
}
|
||||
|
||||
// const startTime = Date.now();
|
||||
// Try job token first
|
||||
try {
|
||||
const jobContext = await validateJobToken(token);
|
||||
return {
|
||||
success: true,
|
||||
userType: 'job',
|
||||
userId: jobContext.userId,
|
||||
orgId: jobContext.orgId || null,
|
||||
jobId: jobContext.jobId,
|
||||
};
|
||||
} catch {
|
||||
// If job token validation fails, try agent token
|
||||
// (existing agent token code would go here)
|
||||
|
||||
// try {
|
||||
// const authHeader = request.headers.get('authorization');
|
||||
|
||||
// if (!authHeader?.startsWith('Bearer ')) {
|
||||
// return {
|
||||
// success: false,
|
||||
// error: 'Unauthorized: Missing authorization header',
|
||||
// };
|
||||
// }
|
||||
|
||||
// const token = authHeader.slice(7);
|
||||
|
||||
// if (!token) {
|
||||
// return {
|
||||
// success: false,
|
||||
// error: 'Unauthorized: Malformed authorization header',
|
||||
// };
|
||||
// }
|
||||
|
||||
// let payload: AgentTokenPayload;
|
||||
|
||||
// try {
|
||||
// payload = await validateAgentToken(token);
|
||||
// } catch {
|
||||
// return { success: false, error: 'Unauthorized: Invalid token' };
|
||||
// }
|
||||
|
||||
// const { agent_id: userId, org_id: orgId } = payload;
|
||||
|
||||
// updateAgentUsage(
|
||||
// userId,
|
||||
// new URL(request.url).pathname,
|
||||
// request.method,
|
||||
// 200,
|
||||
// Date.now() - startTime,
|
||||
// request.headers.get('user-agent') || undefined,
|
||||
// );
|
||||
|
||||
// return { success: true, userType: 'agent', userId, orgId };
|
||||
// } catch (error) {
|
||||
// console.error(
|
||||
// `authorizeApi: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
// );
|
||||
|
||||
// return { success: false, error: 'Unauthorized: Unexpected error' };
|
||||
// }
|
||||
// If both fail, fall back to Clerk
|
||||
return authorize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ import {
|
|||
organizationDefaultSettingsSchema,
|
||||
organizationCloudSettingsSchema,
|
||||
ORGANIZATION_ALLOW_ALL,
|
||||
ORGANIZATION_DEFAULT,
|
||||
} from '@roo-code/types';
|
||||
|
||||
import { AuditLogTargetType, db, orgSettings } from '@roo-code-cloud/db/server';
|
||||
|
||||
import { authorize } from './auth';
|
||||
import { insertAuditLog } from './auditLogs';
|
||||
import { getOrganizationSettingsByOrgId } from '@/lib/server/organizationSettings';
|
||||
|
||||
export async function getOrganizationSettings(): Promise<OrganizationSettings> {
|
||||
const authResult = await authorize();
|
||||
|
|
@ -24,18 +24,7 @@ export async function getOrganizationSettings(): Promise<OrganizationSettings> {
|
|||
throw new Error('Unauthorized');
|
||||
}
|
||||
|
||||
// Organization settings are only available for organizations, not personal accounts
|
||||
if (!authResult.orgId) {
|
||||
return ORGANIZATION_DEFAULT;
|
||||
}
|
||||
|
||||
const settings = await db
|
||||
.select()
|
||||
.from(orgSettings)
|
||||
.where(eq(orgSettings.orgId, authResult.orgId))
|
||||
.limit(1);
|
||||
|
||||
return settings[0] || ORGANIZATION_DEFAULT;
|
||||
return getOrganizationSettingsByOrgId(authResult.orgId);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { authorizeApi } from '@/actions/auth';
|
||||
import { getOrganizationSettings } from '@/actions/organizationSettings';
|
||||
import { getOrganizationSettingsByOrgId } from '@/lib/server/organizationSettings';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
|
|
@ -14,7 +14,7 @@ export async function GET(request: NextRequest) {
|
|||
);
|
||||
}
|
||||
|
||||
const settings = await getOrganizationSettings();
|
||||
const settings = await getOrganizationSettingsByOrgId(authResult.orgId);
|
||||
|
||||
return NextResponse.json(settings);
|
||||
} catch (error) {
|
||||
|
|
|
|||
29
apps/web/src/lib/server/organizationSettings.ts
Normal file
29
apps/web/src/lib/server/organizationSettings.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { eq } from 'drizzle-orm';
|
||||
|
||||
import {
|
||||
type OrganizationSettings,
|
||||
ORGANIZATION_DEFAULT,
|
||||
} from '@roo-code/types';
|
||||
|
||||
import { db, orgSettings } from '@roo-code-cloud/db/server';
|
||||
|
||||
/**
|
||||
* Fetches organization settings by organization ID.
|
||||
* Returns default settings for personal accounts (null orgId) or when no settings exist.
|
||||
*/
|
||||
export async function getOrganizationSettingsByOrgId(
|
||||
orgId: string | null,
|
||||
): Promise<OrganizationSettings> {
|
||||
// Organization settings are only available for organizations, not personal accounts
|
||||
if (!orgId) {
|
||||
return ORGANIZATION_DEFAULT;
|
||||
}
|
||||
|
||||
const settings = await db
|
||||
.select()
|
||||
.from(orgSettings)
|
||||
.where(eq(orgSettings.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
return settings[0] || ORGANIZATION_DEFAULT;
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
// import type { NextRequest } from 'next/server';
|
||||
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
|
||||
const isUnprotectedRoute = createRouteMatcher([
|
||||
'/sign-in(.*)',
|
||||
|
|
@ -10,18 +11,18 @@ const isUnprotectedRoute = createRouteMatcher([
|
|||
'/',
|
||||
]);
|
||||
|
||||
// const isApiRoute = createRouteMatcher(['/api(.*)']);
|
||||
const isApiRoute = createRouteMatcher(['/api(.*)']);
|
||||
|
||||
// const isAgentRequest = (req: NextRequest) => {
|
||||
// return req.headers.get('authorization')?.startsWith('Bearer ');
|
||||
// };
|
||||
const isAgentRequest = (req: NextRequest) => {
|
||||
return req.headers.get('authorization')?.startsWith('Bearer ');
|
||||
};
|
||||
|
||||
export default clerkMiddleware(
|
||||
async (auth, req) => {
|
||||
// const isUnprotected =
|
||||
// isUnprotectedRoute(req) || (isApiRoute(req) && isAgentRequest(req));
|
||||
const isUnprotected =
|
||||
isUnprotectedRoute(req) || (isApiRoute(req) && isAgentRequest(req));
|
||||
|
||||
if (!isUnprotectedRoute(req)) {
|
||||
if (!isUnprotected) {
|
||||
await auth.protect();
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
* UserType
|
||||
*/
|
||||
|
||||
const userTypes = ['user', 'agent'] as const;
|
||||
const userTypes = ['user', 'agent', 'job'] as const;
|
||||
|
||||
export type UserType = (typeof userTypes)[number];
|
||||
|
||||
|
|
@ -48,6 +48,18 @@ export type AgentAuthSuccess = {
|
|||
orgId: string;
|
||||
};
|
||||
|
||||
export type JobAuthSuccess = {
|
||||
success: true;
|
||||
userType: 'job';
|
||||
userId: string;
|
||||
orgId: string | null;
|
||||
jobId: string;
|
||||
};
|
||||
|
||||
export type AuthResult = UserAuthSuccess | AuthError;
|
||||
|
||||
export type ApiAuthResult = UserAuthSuccess | AgentAuthSuccess | AuthError;
|
||||
export type ApiAuthResult =
|
||||
| UserAuthSuccess
|
||||
| AgentAuthSuccess
|
||||
| JobAuthSuccess
|
||||
| AuthError;
|
||||
|
|
|
|||
2
packages/db/drizzle/0002_slippery_alex_power.sql
Normal file
2
packages/db/drizzle/0002_slippery_alex_power.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE "cloud_jobs" ADD COLUMN "user_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "cloud_jobs" ADD CONSTRAINT "cloud_jobs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;
|
||||
1075
packages/db/drizzle/meta/0002_snapshot.json
Normal file
1075
packages/db/drizzle/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -15,6 +15,13 @@
|
|||
"when": 1750703234599,
|
||||
"tag": "0001_noisy_captain_stacy",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "7",
|
||||
"when": 1751397021767,
|
||||
"tag": "0002_slippery_alex_power",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,6 +301,7 @@ export const cloudJobs = pgTable('cloud_jobs', {
|
|||
result: jsonb('result'),
|
||||
error: text('error'),
|
||||
slackThreadTs: text('slack_thread_ts'),
|
||||
userId: text('user_id').references(() => users.id),
|
||||
startedAt: timestamp('started_at'),
|
||||
completedAt: timestamp('completed_at'),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
|
|
|
|||
4
packages/env/src/index.ts
vendored
4
packages/env/src/index.ts
vendored
|
|
@ -11,6 +11,8 @@ export const Env = createEnv({
|
|||
CLICKHOUSE_URL: z.string().min(1),
|
||||
CLICKHOUSE_PASSWORD: z.string().min(1),
|
||||
CLERK_SECRET_KEY: z.string().min(1),
|
||||
JOB_AUTH_PRIVATE_KEY: z.string().min(1),
|
||||
JOB_AUTH_PUBLIC_KEY: z.string().min(1),
|
||||
},
|
||||
client: {},
|
||||
// You need to destructure all the keys manually.
|
||||
|
|
@ -21,5 +23,7 @@ export const Env = createEnv({
|
|||
CLICKHOUSE_URL: process.env.CLICKHOUSE_URL,
|
||||
CLICKHOUSE_PASSWORD: process.env.CLICKHOUSE_PASSWORD,
|
||||
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
||||
JOB_AUTH_PRIVATE_KEY: process.env.JOB_AUTH_PRIVATE_KEY,
|
||||
JOB_AUTH_PUBLIC_KEY: process.env.JOB_AUTH_PUBLIC_KEY,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
4
packages/job-auth/eslint.config.mjs
Normal file
4
packages/job-auth/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@roo-code-cloud/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
24
packages/job-auth/package.json
Normal file
24
packages/job-auth/package.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "@roo-code-cloud/job-auth",
|
||||
"version": "0.1.0",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint src --ext=ts --max-warnings=0",
|
||||
"check-types": "tsc --noEmit",
|
||||
"test": "dotenvx run -f ../../.env.test -- vitest",
|
||||
"clean": "rimraf .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"zod": "^3.22.0",
|
||||
"@roo-code-cloud/env": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsonwebtoken": "^9.0.0",
|
||||
"@roo-code-cloud/config-eslint": "workspace:^",
|
||||
"@roo-code-cloud/config-typescript": "workspace:*",
|
||||
"@types/node": "^24.0.3",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
253
packages/job-auth/src/__tests__/index.test.ts
Normal file
253
packages/job-auth/src/__tests__/index.test.ts
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
// pnpm test --filter @roo-code-cloud/job-auth
|
||||
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
import {
|
||||
createJobToken,
|
||||
validateJobToken,
|
||||
type JobTokenPayload,
|
||||
} from '../index';
|
||||
|
||||
describe('job-auth', () => {
|
||||
const testPrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgVTWTozeXjKjcNk+g
|
||||
zTWjJHlt9W3NOY7Wr2egTq3W/2ehRANCAAQdNAio5NZLvXJ2mcNBkhzcAA63g17y
|
||||
/1uIE2wJYtYN002cxgqqzyWIpZOB0BV9Bm6TIMBbeigHo83EPP4SAuIQ
|
||||
-----END PRIVATE KEY-----`;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock Date.now() to return a consistent timestamp
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2023-01-01T00:00:00Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('createJobToken', () => {
|
||||
it('should create a valid job token', async () => {
|
||||
const jobId = 'job-123';
|
||||
const userId = 'user-456';
|
||||
const orgId = 'org-789';
|
||||
const timeoutMs = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
const token = await createJobToken(jobId, userId, orgId, timeoutMs);
|
||||
|
||||
expect(typeof token).toBe('string');
|
||||
expect(token.split('.')).toHaveLength(3); // JWT has 3 parts
|
||||
|
||||
// Verify the token can be decoded and has correct structure
|
||||
const decoded = jwt.decode(token) as JobTokenPayload;
|
||||
expect(decoded).toEqual({
|
||||
iss: 'rcc',
|
||||
sub: jobId,
|
||||
exp: 1672531200 + 30 * 60 + 5 * 60, // timeout + grace period
|
||||
iat: 1672531200, // 2023-01-01T00:00:00Z in seconds
|
||||
nbf: 1672531200 - 30, // iat - clock skew grace
|
||||
v: 1,
|
||||
r: {
|
||||
u: userId,
|
||||
o: orgId,
|
||||
t: 'cj',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should create token without orgId when null', async () => {
|
||||
const jobId = 'job-123';
|
||||
const userId = 'user-456';
|
||||
const orgId = null;
|
||||
const timeoutMs = 30 * 60 * 1000;
|
||||
|
||||
const token = await createJobToken(jobId, userId, orgId, timeoutMs);
|
||||
const decoded = jwt.decode(token) as JobTokenPayload;
|
||||
|
||||
expect(decoded.r.o).toBeUndefined();
|
||||
expect(decoded.r.u).toBe(userId);
|
||||
});
|
||||
|
||||
it('should use correct issuer', async () => {
|
||||
const token = await createJobToken('job-1', 'user-1', null, 1000);
|
||||
const decoded = jwt.decode(token) as JobTokenPayload;
|
||||
|
||||
expect(decoded.iss).toBe('rcc');
|
||||
});
|
||||
|
||||
it('should set correct expiration time with grace period', async () => {
|
||||
const timeoutMs = 15 * 60 * 1000; // 15 minutes
|
||||
const token = await createJobToken('job-1', 'user-1', null, timeoutMs);
|
||||
const decoded = jwt.decode(token) as JobTokenPayload;
|
||||
|
||||
const expectedExp = decoded.iat + Math.floor(timeoutMs / 1000) + 5 * 60; // timeout + 5 min grace
|
||||
expect(decoded.exp).toBe(expectedExp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateJobToken', () => {
|
||||
const validPayload: JobTokenPayload = {
|
||||
iss: 'rcc',
|
||||
sub: 'job-123',
|
||||
exp: Math.floor(Date.now() / 1000) + 15 * 60,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
nbf: Math.floor(Date.now() / 1000) - 30,
|
||||
v: 1,
|
||||
r: {
|
||||
u: 'user-456',
|
||||
o: 'org-789',
|
||||
t: 'cj',
|
||||
},
|
||||
};
|
||||
|
||||
it('should validate and return context for valid token', async () => {
|
||||
const token = jwt.sign(validPayload, testPrivateKey, {
|
||||
algorithm: 'ES256',
|
||||
});
|
||||
const result = await validateJobToken(token);
|
||||
|
||||
expect(result).toEqual({
|
||||
jobId: 'job-123',
|
||||
userId: 'user-456',
|
||||
orgId: 'org-789',
|
||||
tokenType: 'cj',
|
||||
version: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate token without orgId', async () => {
|
||||
const payloadWithoutOrg = {
|
||||
...validPayload,
|
||||
r: {
|
||||
u: 'user-456',
|
||||
t: 'cj' as const,
|
||||
},
|
||||
};
|
||||
|
||||
const token = jwt.sign(payloadWithoutOrg, testPrivateKey, {
|
||||
algorithm: 'ES256',
|
||||
});
|
||||
const result = await validateJobToken(token);
|
||||
|
||||
expect(result.orgId).toBeUndefined();
|
||||
expect(result.userId).toBe('user-456');
|
||||
});
|
||||
|
||||
it('should throw error for invalid JWT signature', async () => {
|
||||
// Generate a different private key for testing wrong signature
|
||||
const wrongPrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgDCtVZ3X8QqSHLcrp
|
||||
NYfy87xEuVYS3cU3xztnRADyBJKhRANCAASXyME7rnCKJvIN+W9aYEkOjfCUk4Q6
|
||||
G4d1mCKQT05YKo5JdhxbGD3gm3lP/U3NhY+GxA6Dy1BTAYy+dR7prjly
|
||||
-----END PRIVATE KEY-----`;
|
||||
const tokenWithWrongSecret = jwt.sign(validPayload, wrongPrivateKey, {
|
||||
algorithm: 'ES256',
|
||||
});
|
||||
|
||||
await expect(validateJobToken(tokenWithWrongSecret)).rejects.toThrow(
|
||||
'invalid signature',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for expired JWT', async () => {
|
||||
const expiredPayload = {
|
||||
...validPayload,
|
||||
exp: Math.floor(Date.now() / 1000) - 3600, // expired 1 hour ago
|
||||
};
|
||||
const expiredToken = jwt.sign(expiredPayload, testPrivateKey, {
|
||||
algorithm: 'ES256',
|
||||
});
|
||||
|
||||
await expect(validateJobToken(expiredToken)).rejects.toThrow(
|
||||
'jwt expired',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for wrong issuer', async () => {
|
||||
const wrongIssuerPayload = {
|
||||
...validPayload,
|
||||
iss: 'wrong-issuer',
|
||||
};
|
||||
const token = jwt.sign(wrongIssuerPayload, testPrivateKey, {
|
||||
algorithm: 'ES256',
|
||||
});
|
||||
|
||||
await expect(validateJobToken(token)).rejects.toThrow(
|
||||
'jwt issuer invalid. expected: rcc',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw specific error for invalid token structure', async () => {
|
||||
const invalidPayload = {
|
||||
...validPayload,
|
||||
r: {
|
||||
u: '', // Invalid: empty string
|
||||
o: 'org-789',
|
||||
t: 'cj',
|
||||
},
|
||||
};
|
||||
const token = jwt.sign(invalidPayload, testPrivateKey, {
|
||||
algorithm: 'ES256',
|
||||
});
|
||||
|
||||
await expect(validateJobToken(token)).rejects.toThrow(
|
||||
'Invalid job token structure: r.u: User ID is required',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for wrong token type', async () => {
|
||||
const wrongTypePayload = {
|
||||
...validPayload,
|
||||
r: {
|
||||
u: 'user-456',
|
||||
o: 'org-789',
|
||||
t: 'agent', // Should be 'cj'
|
||||
},
|
||||
};
|
||||
const token = jwt.sign(wrongTypePayload, testPrivateKey, {
|
||||
algorithm: 'ES256',
|
||||
});
|
||||
|
||||
await expect(validateJobToken(token)).rejects.toThrow(
|
||||
'Invalid job token structure',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for wrong version', async () => {
|
||||
const wrongVersionPayload = {
|
||||
...validPayload,
|
||||
v: 2, // Should be 1
|
||||
};
|
||||
const token = jwt.sign(wrongVersionPayload, testPrivateKey, {
|
||||
algorithm: 'ES256',
|
||||
});
|
||||
|
||||
await expect(validateJobToken(token)).rejects.toThrow(
|
||||
'Invalid job token structure: v: Version must be 1',
|
||||
);
|
||||
});
|
||||
|
||||
it('should validate token created by createJobToken function', async () => {
|
||||
const jobId = 'test-job-456';
|
||||
const userId = 'test-user-789';
|
||||
const orgId = 'test-org-123';
|
||||
const timeoutMs = 30 * 60 * 1000;
|
||||
|
||||
const token = await createJobToken(jobId, userId, orgId, timeoutMs);
|
||||
const validatedContext = await validateJobToken(token);
|
||||
|
||||
expect(validatedContext.jobId).toBe(jobId);
|
||||
expect(validatedContext.userId).toBe(userId);
|
||||
expect(validatedContext.orgId).toBe(orgId);
|
||||
expect(validatedContext.tokenType).toBe('cj');
|
||||
expect(validatedContext.version).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle malformed JWT', async () => {
|
||||
const malformedToken = 'not.a.valid.jwt.token';
|
||||
|
||||
await expect(validateJobToken(malformedToken)).rejects.toThrow(
|
||||
'jwt malformed',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
74
packages/job-auth/src/index.ts
Normal file
74
packages/job-auth/src/index.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import jwt from 'jsonwebtoken';
|
||||
import { Env } from '@roo-code-cloud/env';
|
||||
import {
|
||||
jobTokenPayloadSchema,
|
||||
type JobTokenPayload,
|
||||
type JobTokenContext,
|
||||
} from './types';
|
||||
|
||||
export * from './types';
|
||||
|
||||
const ISSUER = 'rcc';
|
||||
|
||||
export async function createJobToken(
|
||||
jobId: string,
|
||||
userId: string,
|
||||
orgId: string | null,
|
||||
timeoutMs: number,
|
||||
): Promise<string> {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const gracePeriod = 5 * 60; // 5 minutes
|
||||
const clockSkewGrace = 30; // 30 seconds
|
||||
|
||||
const payload: JobTokenPayload = {
|
||||
iss: ISSUER,
|
||||
sub: jobId,
|
||||
exp: now + Math.floor(timeoutMs / 1000) + gracePeriod,
|
||||
iat: now,
|
||||
nbf: now - clockSkewGrace,
|
||||
v: 1,
|
||||
r: {
|
||||
u: userId,
|
||||
o: orgId || undefined,
|
||||
t: 'cj',
|
||||
},
|
||||
};
|
||||
|
||||
const privateKey = Buffer.from(Env.JOB_AUTH_PRIVATE_KEY, 'base64').toString(
|
||||
'utf-8',
|
||||
);
|
||||
return jwt.sign(payload, privateKey, { algorithm: 'ES256' });
|
||||
}
|
||||
|
||||
export async function validateJobToken(
|
||||
token: string,
|
||||
): Promise<JobTokenContext> {
|
||||
const publicKey = Buffer.from(Env.JOB_AUTH_PUBLIC_KEY, 'base64').toString(
|
||||
'utf-8',
|
||||
);
|
||||
const rawPayload = jwt.verify(token, publicKey, {
|
||||
algorithms: ['ES256'],
|
||||
clockTolerance: 60, // 60 seconds clock skew tolerance for tests
|
||||
ignoreNotBefore: Env.NODE_ENV === 'test', // Ignore nbf in tests due to fake timers
|
||||
issuer: ISSUER,
|
||||
});
|
||||
|
||||
const parseResult = jobTokenPayloadSchema.safeParse(rawPayload);
|
||||
|
||||
if (!parseResult.success) {
|
||||
const validationErrors = parseResult.error.errors
|
||||
.map((err) => `${err.path.join('.')}: ${err.message}`)
|
||||
.join(', ');
|
||||
throw new Error(`Invalid job token structure: ${validationErrors}`);
|
||||
}
|
||||
|
||||
const payload = parseResult.data;
|
||||
|
||||
return {
|
||||
jobId: payload.sub,
|
||||
userId: payload.r.u,
|
||||
orgId: payload.r.o,
|
||||
tokenType: payload.r.t,
|
||||
version: payload.v,
|
||||
};
|
||||
}
|
||||
27
packages/job-auth/src/types.ts
Normal file
27
packages/job-auth/src/types.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const jobTokenPayloadSchema = z.object({
|
||||
iss: z.string().min(1, 'Issuer (iss) is required'),
|
||||
sub: z.string().min(1, 'Subject (sub) is required'), // CloudJob ID
|
||||
exp: z.number().int().positive('Expiration (exp) must be a positive integer'),
|
||||
iat: z.number().int().positive('Issued at (iat) must be a positive integer'),
|
||||
nbf: z.number().int().positive('Not before (nbf) must be a positive integer'),
|
||||
v: z.literal(1, { errorMap: () => ({ message: 'Version must be 1' }) }),
|
||||
r: z.object({
|
||||
u: z.string().min(1, 'User ID is required'),
|
||||
o: z.string().optional(), // Organization ID (optional)
|
||||
t: z.literal('cj', {
|
||||
errorMap: () => ({ message: 'Token type must be "cj"' }),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
export type JobTokenPayload = z.infer<typeof jobTokenPayloadSchema>;
|
||||
|
||||
export interface JobTokenContext {
|
||||
jobId: string;
|
||||
userId: string;
|
||||
orgId?: string;
|
||||
tokenType: 'cj';
|
||||
version: number;
|
||||
}
|
||||
8
packages/job-auth/tsconfig.json
Normal file
8
packages/job-auth/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "@roo-code-cloud/config-typescript/base.json",
|
||||
"compilerOptions": {
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
9
packages/job-auth/vitest.config.ts
Normal file
9
packages/job-auth/vitest.config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
watch: false,
|
||||
environment: 'node',
|
||||
},
|
||||
});
|
||||
34
pnpm-lock.yaml
generated
34
pnpm-lock.yaml
generated
|
|
@ -48,6 +48,9 @@ importers:
|
|||
'@roo-code-cloud/ipc':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/ipc
|
||||
'@roo-code-cloud/job-auth':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/job-auth
|
||||
'@roo-code/types':
|
||||
specifier: ^1.30.0
|
||||
version: 1.30.0
|
||||
|
|
@ -209,6 +212,9 @@ importers:
|
|||
'@roo-code-cloud/env':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/env
|
||||
'@roo-code-cloud/job-auth':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/job-auth
|
||||
'@roo-code/types':
|
||||
specifier: ^1.30.0
|
||||
version: 1.30.0
|
||||
|
|
@ -531,6 +537,34 @@ importers:
|
|||
specifier: ^3.2.3
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
|
||||
|
||||
packages/job-auth:
|
||||
dependencies:
|
||||
'@roo-code-cloud/env':
|
||||
specifier: workspace:*
|
||||
version: link:../env
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.2
|
||||
zod:
|
||||
specifier: ^3.22.0
|
||||
version: 3.25.41
|
||||
devDependencies:
|
||||
'@roo-code-cloud/config-eslint':
|
||||
specifier: workspace:^
|
||||
version: link:../config-eslint
|
||||
'@roo-code-cloud/config-typescript':
|
||||
specifier: workspace:*
|
||||
version: link:../config-typescript
|
||||
'@types/jsonwebtoken':
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.10
|
||||
'@types/node':
|
||||
specifier: ^24.0.3
|
||||
version: 24.0.3
|
||||
vitest:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.0.3)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0)
|
||||
|
||||
packages:
|
||||
|
||||
'@adobe/css-tools@4.4.3':
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue