@waishnav/devspace 1.0.0 → 1.0.2
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/LICENSE +21 -0
- package/README.md +104 -36
- package/dist/cli.js +5 -2
- package/dist/config.js +16 -7
- package/dist/db/client.js +10 -3
- package/dist/db/migrations.js +123 -0
- package/dist/db/schema.js +24 -1
- package/dist/oauth-provider.js +35 -64
- package/dist/oauth-store.js +138 -0
- package/dist/review-checkpoints.js +4 -4
- package/dist/server.js +38 -19
- package/dist/ui/.vite/manifest.json +281 -281
- package/dist/ui/assets/{angular-html-Di31D1W0.js → angular-html-Dj4LysSE.js} +1 -1
- package/dist/ui/assets/{angular-ts-BKuSFrBS.js → angular-ts-CxuC_FFh.js} +1 -1
- package/dist/ui/assets/{apl-DgqYbYoh.js → apl-RwuvNFFo.js} +1 -1
- package/dist/ui/assets/{astro-B19gi6Os.js → astro-ncoaUbsO.js} +1 -1
- package/dist/ui/assets/{blade-BKHjA4oS.js → blade-Dw8-Lzux.js} +1 -1
- package/dist/ui/assets/{c-FjyGimBn.js → c-Dpbbo_f2.js} +1 -1
- package/dist/ui/assets/{cobol-DlmtgjjX.js → cobol-4ddX3THy.js} +1 -1
- package/dist/ui/assets/{coffee-BlTKLwcf.js → coffee-Bn-a5KqK.js} +1 -1
- package/dist/ui/assets/{cpp-DtVUwoaa.js → cpp-DoRrlM_T.js} +1 -1
- package/dist/ui/assets/{crystal-C0TLGTHr.js → crystal-DiZfCxn-.js} +1 -1
- package/dist/ui/assets/{css-FssmIjiX.js → css-DiQoSjDH.js} +1 -1
- package/dist/ui/assets/{edge-_m-12TBz.js → edge-wEhCUoEt.js} +1 -1
- package/dist/ui/assets/{elixir-RKJED3b7.js → elixir-C3obhm0H.js} +1 -1
- package/dist/ui/assets/{elm-CzMSD9Oq.js → elm-DyTBfh1L.js} +1 -1
- package/dist/ui/assets/{erb-Cqpp-3vK.js → erb-f4TnXRuY.js} +1 -1
- package/dist/ui/assets/{git-rebase-DLS2r2oj.js → git-rebase-vhZbWNfp.js} +1 -1
- package/dist/ui/assets/{glimmer-js-B1t-JGK1.js → glimmer-js-BG7puL6Y.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-CazygCNf.js → glimmer-ts-CNCfLLL1.js} +1 -1
- package/dist/ui/assets/{glsl-gBg2vKZ-.js → glsl-DhT76AL5.js} +1 -1
- package/dist/ui/assets/{graphql-C8u6MNtl.js → graphql-CJX08w8y.js} +1 -1
- package/dist/ui/assets/{hack-CgKuqejW.js → hack-DY4NGCCL.js} +1 -1
- package/dist/ui/assets/{haml-I5WU4_Rg.js → haml-DwSkmVcG.js} +1 -1
- package/dist/ui/assets/{handlebars-DGf6O3Q6.js → handlebars-CQ_1dEmi.js} +1 -1
- package/dist/ui/assets/{heavy-payload-Bp6srDbj.js → heavy-payload-2rGBiitA.js} +1 -1
- package/dist/ui/assets/{html-BrIuweS5.js → html-B_e3Njif.js} +1 -1
- package/dist/ui/assets/{html-derivative-CflP_1S6.js → html-derivative-_nHZ_Bk-.js} +1 -1
- package/dist/ui/assets/{http-qgGkv9Ds.js → http-CfcvPIru.js} +1 -1
- package/dist/ui/assets/{hurl-CantyQ5z.js → hurl-9vWDQIMO.js} +1 -1
- package/dist/ui/assets/{java-DqStxnBY.js → java-C4S5r6L5.js} +1 -1
- package/dist/ui/assets/{javascript-CJ1ZUMNM.js → javascript-BRwqbMMC.js} +1 -1
- package/dist/ui/assets/{jinja-Ta-y-8tW.js → jinja-DHxzMMc5.js} +1 -1
- package/dist/ui/assets/{jison-DZStAtdf.js → jison-B1DqeWXb.js} +1 -1
- package/dist/ui/assets/{json-Bfn_pgvS.js → json-DcFTkEdd.js} +1 -1
- package/dist/ui/assets/{jsx-CFi5D1JI.js → jsx-DgfmYa9f.js} +1 -1
- package/dist/ui/assets/{julia-CmuPb_FV.js → julia-3M8SJ8iF.js} +1 -1
- package/dist/ui/assets/{just-xoDaYyy4.js → just-UyH7_DNz.js} +1 -1
- package/dist/ui/assets/{latex-C7jbQT4s.js → latex-wUbha6EX.js} +1 -1
- package/dist/ui/assets/{liquid-DyVckOju.js → liquid-DqNTB9kN.js} +1 -1
- package/dist/ui/assets/{lua-CQGEo-kr.js → lua-DrEQ1QTW.js} +1 -1
- package/dist/ui/assets/{marko-CF3FIlcL.js → marko-B3GJw9Md.js} +1 -1
- package/dist/ui/assets/{mdc-DJfBh8KU.js → mdc-BnThk82m.js} +1 -1
- package/dist/ui/assets/{nginx-CmN5T-d3.js → nginx-B5IeSsOX.js} +1 -1
- package/dist/ui/assets/{nim-a_705mqZ.js → nim-BJraatnS.js} +1 -1
- package/dist/ui/assets/{perl-BFxFi55O.js → perl-BAhrR4gt.js} +1 -1
- package/dist/ui/assets/{php-C9Uf0Vo_.js → php-BKRCk_wX.js} +1 -1
- package/dist/ui/assets/{pug-DQIrtlYh.js → pug-B8gmSV7z.js} +1 -1
- package/dist/ui/assets/{qml-CpI_Asuw.js → qml-DKrxIjHR.js} +1 -1
- package/dist/ui/assets/{r-hYZdyIMH.js → r-YASBZvTH.js} +1 -1
- package/dist/ui/assets/{razor-CbXx350T.js → razor-P-XFZqh6.js} +1 -1
- package/dist/ui/assets/{regexp-BdfO5R2u.js → regexp-CCpiH6nO.js} +1 -1
- package/dist/ui/assets/{review-payload-CC-3-Lb9.js → review-payload-BLYwg6t8.js} +1 -1
- package/dist/ui/assets/{rst-CP6mAm3Y.js → rst-CFAzckbn.js} +1 -1
- package/dist/ui/assets/{ruby-8tjXzrRr.js → ruby-Dyp_Rx-S.js} +1 -1
- package/dist/ui/assets/{sas-rqWHY37U.js → sas-YkWZIXxT.js} +1 -1
- package/dist/ui/assets/{scss-aGYgGui9.js → scss-Cf89idGl.js} +1 -1
- package/dist/ui/assets/{shellscript-CYr5JWBI.js → shellscript-BFMb52q8.js} +1 -1
- package/dist/ui/assets/{shellsession-CO3ljRR7.js → shellsession-DuthW-5v.js} +1 -1
- package/dist/ui/assets/{soy-C4_rLmHV.js → soy-BohVVlJU.js} +1 -1
- package/dist/ui/assets/{sql-2jcbKHJ9.js → sql-CnK9R2Gt.js} +1 -1
- package/dist/ui/assets/{stata-Dbr0lM_v.js → stata-D-sV4Vbr.js} +1 -1
- package/dist/ui/assets/{surrealql-Cp0eMxMo.js → surrealql-ChAy0gHv.js} +1 -1
- package/dist/ui/assets/{svelte-BfsT9lcS.js → svelte-3HBVqG-H.js} +1 -1
- package/dist/ui/assets/{templ-BBOwNqeW.js → templ-BixlV4Cp.js} +1 -1
- package/dist/ui/assets/{tex-CPmn_RsX.js → tex-DWoJKHvD.js} +1 -1
- package/dist/ui/assets/{ts-tags-DMILlUz_.js → ts-tags-DLA8eH13.js} +1 -1
- package/dist/ui/assets/{tsx-CUP2Lfiz.js → tsx-BYWa5JUz.js} +1 -1
- package/dist/ui/assets/{twig-DEZ_7SDX.js → twig-CnY65vAo.js} +1 -1
- package/dist/ui/assets/{typescript-DhaMQEhQ.js → typescript-ByInUsro.js} +1 -1
- package/dist/ui/assets/{useFileDiffInstance-Bo6irrE6.js → useFileDiffInstance-Dg0pWcJV.js} +3 -3
- package/dist/ui/assets/{vue-CAaS6wtt.js → vue-Qngt2rkz.js} +1 -1
- package/dist/ui/assets/{vue-html-D3Etensv.js → vue-html-DDue9upr.js} +1 -1
- package/dist/ui/assets/{vue-vine-DNRKj0Lh.js → vue-vine-DBcAKsA6.js} +1 -1
- package/dist/ui/assets/workspace-app-DGD9R89D.css +1 -0
- package/dist/ui/assets/{workspace-app-BLeU1qC_.js → workspace-app-DuzpU4Hf.js} +6 -6
- package/dist/ui/assets/{xml-M-X6I2aP.js → xml-CRLR5jiL.js} +1 -1
- package/dist/ui/assets/{xsl-CR0ZlrL7.js → xsl-CmcwyXbm.js} +1 -1
- package/dist/ui/assets/{yaml-pI4xDDgj.js → yaml-Dpe6Eg-k.js} +1 -1
- package/dist/ui/workspace-app.html +2 -2
- package/dist/workspace-store.js +0 -50
- package/docs/chatgpt-coding-workflow.md +22 -22
- package/docs/configuration.md +13 -10
- package/docs/gotchas.md +5 -4
- package/package.json +3 -5
- package/scripts/dev-server.mjs +120 -0
- package/dist/ui/assets/workspace-app-8--oTbCd.css +0 -1
- package/scripts/build-release.mjs +0 -89
- package/scripts/ensure-native-deps.mjs +0 -29
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Waishnav
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<p align="center">
|
|
2
2
|
<picture>
|
|
3
|
-
<img src="docs/assets/devspace-logo-light.png" alt="DevSpace logo" width="140">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Waishnav/devspace/main/docs/assets/devspace-logo-light.png" alt="DevSpace logo" width="140">
|
|
4
4
|
</picture>
|
|
5
5
|
</p>
|
|
6
6
|
|
|
@@ -14,16 +14,66 @@
|
|
|
14
14
|
<a href="https://github.com/Waishnav/devspace/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/npm/l/%40waishnav%2Fdevspace?style=flat-square" /></a>
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
|
-
[](docs/assets/devspace-screenshot.png)
|
|
17
|
+
[](https://raw.githubusercontent.com/Waishnav/devspace/main/docs/assets/devspace-screenshot.png)
|
|
18
18
|
|
|
19
19
|
**Give ChatGPT a secure connection to your own machine and Turn ChatGPT into Codex**
|
|
20
20
|
|
|
21
21
|
DevSpace is a self-hosted MCP server that lets ChatGPT read, edit, search, and run code in your real local projects — your files, your tools, your terminal — without uploading anything to a third party. You run it on your machine, expose it through a tunnel you control, and approve the connection with a password only you have.
|
|
22
22
|
|
|
23
|
-
##
|
|
23
|
+
## Sponsors and Special Thanks
|
|
24
|
+
|
|
25
|
+
<table>
|
|
26
|
+
<thead>
|
|
27
|
+
<tr>
|
|
28
|
+
<th>Sponsor</th>
|
|
29
|
+
<th>About</th>
|
|
30
|
+
</tr>
|
|
31
|
+
</thead>
|
|
32
|
+
<tbody>
|
|
33
|
+
<tr>
|
|
34
|
+
<td align="center" width="220">
|
|
35
|
+
<a href="https://rebates.ai/">
|
|
36
|
+
<img
|
|
37
|
+
src="https://app.rebates.ai/brand/rebates-lockup.svg"
|
|
38
|
+
alt="Rebates"
|
|
39
|
+
width="170"
|
|
40
|
+
>
|
|
41
|
+
</a>
|
|
42
|
+
</td>
|
|
43
|
+
<td>
|
|
44
|
+
<strong>The ads in your terminal pay you.</strong><br><br>
|
|
45
|
+
<a href="https://rebates.ai/">Rebates</a> adds one optional
|
|
46
|
+
sponsored footer to your coding agent and pays you cash back for every
|
|
47
|
+
session in which it is shown. Turn it off at any time.
|
|
48
|
+
</td>
|
|
49
|
+
</tr>
|
|
50
|
+
</tbody>
|
|
51
|
+
</table>
|
|
52
|
+
|
|
53
|
+
<p align="center">
|
|
54
|
+
DevSpace is open to new sponsors.
|
|
55
|
+
<a href="https://x.com/wshxnv">Get in touch to become one.</a>
|
|
56
|
+
</p>
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
24
59
|
|
|
25
60
|
DevSpace requires Node `>=20.12 <27`. Node 22 LTS is recommended.
|
|
26
61
|
|
|
62
|
+
Install the DevSpace CLI:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install -g @waishnav/devspace
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Then initialize and start the server:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
devspace init
|
|
72
|
+
devspace serve
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or run it without a global install:
|
|
76
|
+
|
|
27
77
|
```bash
|
|
28
78
|
npx @waishnav/devspace init
|
|
29
79
|
npx @waishnav/devspace serve
|
|
@@ -42,11 +92,7 @@ Use the public origin without `/mcp` during setup:
|
|
|
42
92
|
https://your-tunnel-host.example.com
|
|
43
93
|
```
|
|
44
94
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
```text
|
|
48
|
-
https://your-tunnel-host.example.com/mcp
|
|
49
|
-
```
|
|
95
|
+
You will configure your MCP client with the public `/mcp` URL after setup.
|
|
50
96
|
|
|
51
97
|
When the client connects, DevSpace opens an Owner password approval page. Enter
|
|
52
98
|
the Owner password printed by `devspace init`. It is also stored in:
|
|
@@ -57,20 +103,7 @@ the Owner password printed by `devspace init`. It is also stored in:
|
|
|
57
103
|
|
|
58
104
|
Keep that password private.
|
|
59
105
|
|
|
60
|
-
##
|
|
61
|
-
|
|
62
|
-
After the MCP client connects, ChatGPT can open a project with
|
|
63
|
-
`open_workspace` and then reuse the returned `workspaceId` for later calls.
|
|
64
|
-
|
|
65
|
-
DevSpace provides tools for:
|
|
66
|
-
|
|
67
|
-
- reading, writing, and editing files inside the opened workspace
|
|
68
|
-
- searching files and listing directories
|
|
69
|
-
- running shell commands for tests, builds, git, and package scripts
|
|
70
|
-
- opening isolated Git worktrees when you want parallel work
|
|
71
|
-
- loading `AGENTS.md` and `CLAUDE.md` instructions
|
|
72
|
-
- exposing local agent skills from your skill folders
|
|
73
|
-
- showing ChatGPT Apps review cards for aggregate diffs
|
|
106
|
+
## Connect Your MCP Client
|
|
74
107
|
|
|
75
108
|
The default local endpoint is:
|
|
76
109
|
|
|
@@ -84,6 +117,22 @@ Most users should connect through a public HTTPS tunnel:
|
|
|
84
117
|
https://your-tunnel-host.example.com/mcp
|
|
85
118
|
```
|
|
86
119
|
|
|
120
|
+
## What ChatGPT Can Do
|
|
121
|
+
|
|
122
|
+
Once connected, ChatGPT can open one of your approved project folders as a
|
|
123
|
+
workspace. From there, it can inspect the repo, make scoped edits, run commands,
|
|
124
|
+
and show you what changed.
|
|
125
|
+
|
|
126
|
+
DevSpace gives ChatGPT tools to:
|
|
127
|
+
|
|
128
|
+
- read, write, and edit files inside the opened workspace
|
|
129
|
+
- search code and inspect directories
|
|
130
|
+
- run shell commands for tests, builds, git, and package scripts
|
|
131
|
+
- use isolated Git worktrees for parallel coding sessions
|
|
132
|
+
- follow project instructions from `AGENTS.md` and `CLAUDE.md`
|
|
133
|
+
- discover local agent skills from your skill folders
|
|
134
|
+
- show tool cards and optional change summaries in ChatGPT Apps-compatible hosts
|
|
135
|
+
|
|
87
136
|
## Mental Model
|
|
88
137
|
|
|
89
138
|
DevSpace is remote access to selected local folders.
|
|
@@ -95,7 +144,7 @@ connected client like a trusted coding partner with access to your machine.
|
|
|
95
144
|
For a normal ChatGPT coding session:
|
|
96
145
|
|
|
97
146
|
1. Start your tunnel.
|
|
98
|
-
2. Run `
|
|
147
|
+
2. Run `devspace serve`.
|
|
99
148
|
3. Connect the MCP client to your public `/mcp` URL.
|
|
100
149
|
4. Approve the connection with the Owner password.
|
|
101
150
|
5. Ask ChatGPT to open a project inside one of your allowed roots.
|
|
@@ -115,16 +164,41 @@ shell.
|
|
|
115
164
|
Run this to inspect your local setup:
|
|
116
165
|
|
|
117
166
|
```bash
|
|
118
|
-
|
|
167
|
+
devspace doctor
|
|
119
168
|
```
|
|
120
169
|
|
|
121
170
|
## Documentation
|
|
122
171
|
|
|
123
|
-
- [Setup Guide](docs/setup.md)
|
|
124
|
-
- [ChatGPT Coding Workflow](docs/chatgpt-coding-workflow.md)
|
|
125
|
-
- [Configuration Reference](docs/configuration.md)
|
|
126
|
-
- [Security Model](docs/security.md)
|
|
127
|
-
- [Troubleshooting Gotchas](docs/gotchas.md)
|
|
172
|
+
- [Setup Guide](https://github.com/Waishnav/devspace/blob/main/docs/setup.md)
|
|
173
|
+
- [ChatGPT Coding Workflow](https://github.com/Waishnav/devspace/blob/main/docs/chatgpt-coding-workflow.md)
|
|
174
|
+
- [Configuration Reference](https://github.com/Waishnav/devspace/blob/main/docs/configuration.md)
|
|
175
|
+
- [Security Model](https://github.com/Waishnav/devspace/blob/main/docs/security.md)
|
|
176
|
+
- [Troubleshooting Gotchas](https://github.com/Waishnav/devspace/blob/main/docs/gotchas.md)
|
|
177
|
+
|
|
178
|
+
## Philosophy
|
|
179
|
+
|
|
180
|
+
Every piece of software is becoming conversational. Natural language is
|
|
181
|
+
redefining how we interact with tools, workflows, and systems.
|
|
182
|
+
|
|
183
|
+
My bet is that ChatGPT becomes the operating system for everything. Once we
|
|
184
|
+
reach AGI, we will simply talk to ChatGPT, and it will prompt, coordinate, and
|
|
185
|
+
orchestrate sub-agents that set up the right loops for us.
|
|
186
|
+
|
|
187
|
+
We are not there yet.
|
|
188
|
+
|
|
189
|
+
DevSpace is one attempt to fast-forward that future: a way for MCP-capable
|
|
190
|
+
hosts like ChatGPT and Claude to work directly with local project files through
|
|
191
|
+
explicit, inspectable tools.
|
|
192
|
+
|
|
193
|
+
## Built by Waishnav
|
|
194
|
+
|
|
195
|
+
I'm Waishnav, the creator of [GitCMS](https://gitcms.dev/), a Git-backed CMS
|
|
196
|
+
for markdown sites.
|
|
197
|
+
|
|
198
|
+
I like building opinionated products, and DevSpace is another example of that.
|
|
199
|
+
I'm on a journey to build a single-person company doing multiple millions in
|
|
200
|
+
revenue. If you want to watch the failures, wins, lessons, and everything in
|
|
201
|
+
between, come hang out with me on [X](https://x.com/wshxnv).
|
|
128
202
|
|
|
129
203
|
## Local Development
|
|
130
204
|
|
|
@@ -132,15 +206,9 @@ For working on DevSpace itself:
|
|
|
132
206
|
|
|
133
207
|
```bash
|
|
134
208
|
npm install --include=dev
|
|
209
|
+
npm run dev
|
|
135
210
|
npm run typecheck
|
|
136
211
|
npm test
|
|
137
212
|
npm run build
|
|
138
213
|
npm run start
|
|
139
214
|
```
|
|
140
|
-
|
|
141
|
-
For long-running local server processes, build an immutable release copy:
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
npm run release:build
|
|
145
|
-
npm run release:start
|
|
146
|
-
```
|
package/dist/cli.js
CHANGED
|
@@ -149,7 +149,7 @@ async function serve() {
|
|
|
149
149
|
}
|
|
150
150
|
const { createServer } = await import("./server.js");
|
|
151
151
|
const config = loadConfig();
|
|
152
|
-
const { app } = createServer(config);
|
|
152
|
+
const { app, close } = createServer(config);
|
|
153
153
|
const httpServer = app.listen(config.port, config.host, () => {
|
|
154
154
|
console.log(`devspace listening on http://${config.host}:${config.port}/mcp`);
|
|
155
155
|
console.log(`public base url: ${config.publicBaseUrl}`);
|
|
@@ -162,7 +162,10 @@ async function serve() {
|
|
|
162
162
|
console.log(`logging: ${config.logging.level} ${config.logging.format}`);
|
|
163
163
|
});
|
|
164
164
|
const shutdown = () => {
|
|
165
|
-
httpServer.close(() =>
|
|
165
|
+
httpServer.close(() => {
|
|
166
|
+
close();
|
|
167
|
+
process.exit(0);
|
|
168
|
+
});
|
|
166
169
|
};
|
|
167
170
|
process.once("SIGINT", shutdown);
|
|
168
171
|
process.once("SIGTERM", shutdown);
|
package/dist/config.js
CHANGED
|
@@ -45,7 +45,16 @@ function parseBoolean(value) {
|
|
|
45
45
|
return ["1", "true", "yes", "on"].includes(value?.toLowerCase() ?? "");
|
|
46
46
|
}
|
|
47
47
|
function parseMinimalTools(env) {
|
|
48
|
-
|
|
48
|
+
if (env.DEVSPACE_TOOL_MODE === "minimal")
|
|
49
|
+
return true;
|
|
50
|
+
if (env.DEVSPACE_TOOL_MODE === "full")
|
|
51
|
+
return false;
|
|
52
|
+
if (env.DEVSPACE_TOOL_MODE) {
|
|
53
|
+
throw new Error(`Invalid DEVSPACE_TOOL_MODE: ${env.DEVSPACE_TOOL_MODE}`);
|
|
54
|
+
}
|
|
55
|
+
if (env.DEVSPACE_MINIMAL_TOOLS !== undefined)
|
|
56
|
+
return parseBoolean(env.DEVSPACE_MINIMAL_TOOLS);
|
|
57
|
+
return true;
|
|
49
58
|
}
|
|
50
59
|
function parseLogLevel(value) {
|
|
51
60
|
if (!value || value === "info")
|
|
@@ -85,10 +94,10 @@ function parsePositiveInteger(value, fallback, name) {
|
|
|
85
94
|
return parsed;
|
|
86
95
|
}
|
|
87
96
|
function parseToolNaming(value) {
|
|
88
|
-
if (!value || value === "
|
|
89
|
-
return "legacy";
|
|
90
|
-
if (value === "short")
|
|
97
|
+
if (!value || value === "short")
|
|
91
98
|
return "short";
|
|
99
|
+
if (value === "legacy")
|
|
100
|
+
return "legacy";
|
|
92
101
|
throw new Error(`Invalid DEVSPACE_TOOL_NAMING: ${value}`);
|
|
93
102
|
}
|
|
94
103
|
function parseLoggingConfig(env) {
|
|
@@ -103,9 +112,9 @@ function parseLoggingConfig(env) {
|
|
|
103
112
|
};
|
|
104
113
|
}
|
|
105
114
|
function parseWidgetMode(value) {
|
|
106
|
-
if (!value || value === "
|
|
107
|
-
return "
|
|
108
|
-
if (value === "off" || value === "
|
|
115
|
+
if (!value || value === "full")
|
|
116
|
+
return "full";
|
|
117
|
+
if (value === "off" || value === "changes")
|
|
109
118
|
return value;
|
|
110
119
|
throw new Error(`Invalid DEVSPACE_WIDGETS: ${value}`);
|
|
111
120
|
}
|
package/dist/db/client.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import { mkdirSync } from "node:fs";
|
|
1
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import Database from "better-sqlite3";
|
|
4
4
|
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
5
5
|
import * as schema from "./schema.js";
|
|
6
|
+
import { migrateDatabase } from "./migrations.js";
|
|
6
7
|
export function databasePath(stateDir) {
|
|
7
8
|
return join(stateDir, "devspace.sqlite");
|
|
8
9
|
}
|
|
9
10
|
export function openDatabase(stateDir) {
|
|
10
|
-
mkdirSync(stateDir, { recursive: true });
|
|
11
|
-
|
|
11
|
+
mkdirSync(stateDir, { recursive: true, mode: 0o700 });
|
|
12
|
+
chmodSync(stateDir, 0o700);
|
|
13
|
+
const path = databasePath(stateDir);
|
|
14
|
+
const sqlite = new Database(path);
|
|
15
|
+
chmodSync(path, 0o600);
|
|
12
16
|
sqlite.pragma("journal_mode = WAL");
|
|
17
|
+
sqlite.pragma("synchronous = NORMAL");
|
|
18
|
+
sqlite.pragma("busy_timeout = 5000");
|
|
13
19
|
sqlite.pragma("foreign_keys = ON");
|
|
20
|
+
migrateDatabase(sqlite);
|
|
14
21
|
return {
|
|
15
22
|
sqlite,
|
|
16
23
|
db: createDrizzleDatabase(sqlite),
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const migrations = [
|
|
2
|
+
{
|
|
3
|
+
version: 1,
|
|
4
|
+
name: "workspace-state",
|
|
5
|
+
up: migrateWorkspaceState,
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
version: 2,
|
|
9
|
+
name: "oauth-state",
|
|
10
|
+
up: migrateOAuthState,
|
|
11
|
+
},
|
|
12
|
+
];
|
|
13
|
+
export function migrateDatabase(sqlite) {
|
|
14
|
+
const migrate = sqlite.transaction(() => {
|
|
15
|
+
sqlite.exec(`
|
|
16
|
+
create table if not exists devspace_schema_migrations (
|
|
17
|
+
version integer primary key,
|
|
18
|
+
name text not null,
|
|
19
|
+
applied_at text not null
|
|
20
|
+
);
|
|
21
|
+
`);
|
|
22
|
+
const applied = new Set(sqlite.prepare("select version from devspace_schema_migrations").all().map((row) => row.version));
|
|
23
|
+
const recordMigration = sqlite.prepare("insert into devspace_schema_migrations (version, name, applied_at) values (?, ?, ?)");
|
|
24
|
+
for (const migration of migrations) {
|
|
25
|
+
if (applied.has(migration.version))
|
|
26
|
+
continue;
|
|
27
|
+
migration.up(sqlite);
|
|
28
|
+
recordMigration.run(migration.version, migration.name, new Date().toISOString());
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
migrate.immediate();
|
|
32
|
+
}
|
|
33
|
+
function migrateWorkspaceState(sqlite) {
|
|
34
|
+
sqlite.exec(`
|
|
35
|
+
create table if not exists workspace_sessions (
|
|
36
|
+
id text primary key,
|
|
37
|
+
root text not null,
|
|
38
|
+
status text not null default 'active',
|
|
39
|
+
mode text not null default 'checkout',
|
|
40
|
+
source_root text,
|
|
41
|
+
base_ref text,
|
|
42
|
+
base_sha text,
|
|
43
|
+
managed text not null default 'false',
|
|
44
|
+
created_at text not null,
|
|
45
|
+
last_used_at text not null
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
create index if not exists workspace_sessions_root_idx
|
|
49
|
+
on workspace_sessions(root, last_used_at desc);
|
|
50
|
+
|
|
51
|
+
create index if not exists workspace_sessions_status_idx
|
|
52
|
+
on workspace_sessions(status, last_used_at desc);
|
|
53
|
+
|
|
54
|
+
create table if not exists loaded_agent_files (
|
|
55
|
+
workspace_session_id text not null,
|
|
56
|
+
path text not null,
|
|
57
|
+
content_hash text not null,
|
|
58
|
+
content text not null,
|
|
59
|
+
loaded_at text not null,
|
|
60
|
+
last_seen_at text not null,
|
|
61
|
+
primary key (workspace_session_id, path),
|
|
62
|
+
foreign key (workspace_session_id)
|
|
63
|
+
references workspace_sessions(id)
|
|
64
|
+
on delete cascade
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
create index if not exists loaded_agent_files_path_idx
|
|
68
|
+
on loaded_agent_files(path);
|
|
69
|
+
`);
|
|
70
|
+
addColumnIfMissing(sqlite, "workspace_sessions", "mode", "text not null default 'checkout'");
|
|
71
|
+
addColumnIfMissing(sqlite, "workspace_sessions", "source_root", "text");
|
|
72
|
+
addColumnIfMissing(sqlite, "workspace_sessions", "base_ref", "text");
|
|
73
|
+
addColumnIfMissing(sqlite, "workspace_sessions", "base_sha", "text");
|
|
74
|
+
addColumnIfMissing(sqlite, "workspace_sessions", "managed", "text not null default 'false'");
|
|
75
|
+
}
|
|
76
|
+
function migrateOAuthState(sqlite) {
|
|
77
|
+
sqlite.exec(`
|
|
78
|
+
create table if not exists oauth_clients (
|
|
79
|
+
client_id text primary key,
|
|
80
|
+
client_json text not null,
|
|
81
|
+
issued_at integer not null
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
create index if not exists oauth_clients_issued_at_idx
|
|
85
|
+
on oauth_clients(issued_at desc);
|
|
86
|
+
|
|
87
|
+
create table if not exists oauth_access_tokens (
|
|
88
|
+
token_hash text primary key,
|
|
89
|
+
client_id text not null,
|
|
90
|
+
scopes_json text not null,
|
|
91
|
+
expires_at integer not null,
|
|
92
|
+
resource text,
|
|
93
|
+
foreign key (client_id) references oauth_clients(client_id) on delete cascade
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
create index if not exists oauth_access_tokens_client_id_idx
|
|
97
|
+
on oauth_access_tokens(client_id);
|
|
98
|
+
|
|
99
|
+
create index if not exists oauth_access_tokens_expires_at_idx
|
|
100
|
+
on oauth_access_tokens(expires_at);
|
|
101
|
+
|
|
102
|
+
create table if not exists oauth_refresh_tokens (
|
|
103
|
+
token_hash text primary key,
|
|
104
|
+
client_id text not null,
|
|
105
|
+
scopes_json text not null,
|
|
106
|
+
expires_at integer not null,
|
|
107
|
+
resource text,
|
|
108
|
+
foreign key (client_id) references oauth_clients(client_id) on delete cascade
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
create index if not exists oauth_refresh_tokens_client_id_idx
|
|
112
|
+
on oauth_refresh_tokens(client_id);
|
|
113
|
+
|
|
114
|
+
create index if not exists oauth_refresh_tokens_expires_at_idx
|
|
115
|
+
on oauth_refresh_tokens(expires_at);
|
|
116
|
+
`);
|
|
117
|
+
}
|
|
118
|
+
function addColumnIfMissing(sqlite, table, column, definition) {
|
|
119
|
+
const columns = sqlite.prepare(`pragma table_info(${table})`).all();
|
|
120
|
+
if (columns.some((existingColumn) => existingColumn.name === column))
|
|
121
|
+
return;
|
|
122
|
+
sqlite.exec(`alter table ${table} add column ${column} ${definition}`);
|
|
123
|
+
}
|
package/dist/db/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { index, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
1
|
+
import { index, integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
2
2
|
export const workspaceSessions = sqliteTable("workspace_sessions", {
|
|
3
3
|
id: text("id").primaryKey(),
|
|
4
4
|
root: text("root").notNull(),
|
|
@@ -27,3 +27,26 @@ export const loadedAgentFiles = sqliteTable("loaded_agent_files", {
|
|
|
27
27
|
primaryKey({ columns: [table.workspaceSessionId, table.path] }),
|
|
28
28
|
index("loaded_agent_files_path_idx").on(table.path),
|
|
29
29
|
]);
|
|
30
|
+
export const oauthClients = sqliteTable("oauth_clients", {
|
|
31
|
+
clientId: text("client_id").primaryKey(),
|
|
32
|
+
clientJson: text("client_json").notNull(),
|
|
33
|
+
issuedAt: integer("issued_at").notNull(),
|
|
34
|
+
});
|
|
35
|
+
export const oauthAccessTokens = sqliteTable("oauth_access_tokens", {
|
|
36
|
+
tokenHash: text("token_hash").primaryKey(),
|
|
37
|
+
clientId: text("client_id")
|
|
38
|
+
.notNull()
|
|
39
|
+
.references(() => oauthClients.clientId, { onDelete: "cascade" }),
|
|
40
|
+
scopesJson: text("scopes_json").notNull(),
|
|
41
|
+
expiresAt: integer("expires_at").notNull(),
|
|
42
|
+
resource: text("resource"),
|
|
43
|
+
});
|
|
44
|
+
export const oauthRefreshTokens = sqliteTable("oauth_refresh_tokens", {
|
|
45
|
+
tokenHash: text("token_hash").primaryKey(),
|
|
46
|
+
clientId: text("client_id")
|
|
47
|
+
.notNull()
|
|
48
|
+
.references(() => oauthClients.clientId, { onDelete: "cascade" }),
|
|
49
|
+
scopesJson: text("scopes_json").notNull(),
|
|
50
|
+
expiresAt: integer("expires_at").notNull(),
|
|
51
|
+
resource: text("resource"),
|
|
52
|
+
});
|
package/dist/oauth-provider.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { timingSafeEqual, randomBytes, randomUUID, createHash } from "node:crypto";
|
|
2
2
|
import { AccessDeniedError, InvalidGrantError, InvalidRequestError, InvalidTokenError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
3
3
|
import { checkResourceAllowed, resourceUrlFromServerUrl } from "@modelcontextprotocol/sdk/shared/auth-utils.js";
|
|
4
|
+
import { SqliteOAuthClientsStore, SqliteOAuthStore } from "./oauth-store.js";
|
|
4
5
|
const CODE_TTL_MS = 5 * 60 * 1000;
|
|
5
6
|
function randomToken() {
|
|
6
7
|
return randomBytes(32).toString("base64url");
|
|
@@ -74,55 +75,17 @@ ${hiddenFields}
|
|
|
74
75
|
function requestedScopesAllowed(requested, supported) {
|
|
75
76
|
return requested.every((scope) => supported.includes(scope));
|
|
76
77
|
}
|
|
77
|
-
function redirectHostAllowed(redirectUri, allowedHosts) {
|
|
78
|
-
let parsed;
|
|
79
|
-
try {
|
|
80
|
-
parsed = new URL(redirectUri);
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
return false;
|
|
84
|
-
}
|
|
85
|
-
if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname))
|
|
86
|
-
return true;
|
|
87
|
-
return allowedHosts.includes(parsed.hostname);
|
|
88
|
-
}
|
|
89
|
-
export class InMemoryOAuthClientsStore {
|
|
90
|
-
allowedRedirectHosts;
|
|
91
|
-
clients = new Map();
|
|
92
|
-
constructor(allowedRedirectHosts) {
|
|
93
|
-
this.allowedRedirectHosts = allowedRedirectHosts;
|
|
94
|
-
}
|
|
95
|
-
getClient(clientId) {
|
|
96
|
-
return this.clients.get(clientId);
|
|
97
|
-
}
|
|
98
|
-
registerClient(client) {
|
|
99
|
-
if (!client.redirect_uris.every((uri) => redirectHostAllowed(uri, this.allowedRedirectHosts))) {
|
|
100
|
-
throw new InvalidRequestError("Client redirect_uri is not allowed for this DevSpace server");
|
|
101
|
-
}
|
|
102
|
-
const now = Math.floor(Date.now() / 1000);
|
|
103
|
-
const registered = {
|
|
104
|
-
...client,
|
|
105
|
-
client_id: `devspace-${randomUUID()}`,
|
|
106
|
-
client_id_issued_at: now,
|
|
107
|
-
token_endpoint_auth_method: client.token_endpoint_auth_method ?? "none",
|
|
108
|
-
grant_types: client.grant_types ?? ["authorization_code", "refresh_token"],
|
|
109
|
-
response_types: client.response_types ?? ["code"],
|
|
110
|
-
};
|
|
111
|
-
this.clients.set(registered.client_id, registered);
|
|
112
|
-
return registered;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
78
|
export class SingleUserOAuthProvider {
|
|
116
79
|
config;
|
|
117
80
|
clientsStore;
|
|
118
81
|
codes = new Map();
|
|
119
|
-
|
|
120
|
-
refreshTokens = new Map();
|
|
82
|
+
oauthStore;
|
|
121
83
|
resourceServerUrl;
|
|
122
|
-
constructor(config, resourceServerUrl) {
|
|
84
|
+
constructor(config, resourceServerUrl, stateDir) {
|
|
123
85
|
this.config = config;
|
|
124
86
|
this.resourceServerUrl = resourceUrlFromServerUrl(resourceServerUrl);
|
|
125
|
-
this.
|
|
87
|
+
this.oauthStore = new SqliteOAuthStore(stateDir);
|
|
88
|
+
this.clientsStore = new SqliteOAuthClientsStore(this.oauthStore, config.allowedRedirectHosts);
|
|
126
89
|
}
|
|
127
90
|
async authorize(client, params, res) {
|
|
128
91
|
if (!params.resource || !checkResourceAllowed({ requestedResource: params.resource, configuredResource: this.resourceServerUrl })) {
|
|
@@ -181,7 +144,8 @@ export class SingleUserOAuthProvider {
|
|
|
181
144
|
return this.issueTokens(client.client_id, record.params.scopes ?? this.config.scopes, record.params.resource);
|
|
182
145
|
}
|
|
183
146
|
async exchangeRefreshToken(client, refreshToken, scopes, resource) {
|
|
184
|
-
const
|
|
147
|
+
const refreshTokenHash = hashToken(refreshToken);
|
|
148
|
+
const record = this.oauthStore.getRefreshToken(refreshTokenHash);
|
|
185
149
|
if (!record || record.clientId !== client.client_id || record.expiresAt < Math.floor(Date.now() / 1000)) {
|
|
186
150
|
throw new InvalidGrantError("Invalid refresh token");
|
|
187
151
|
}
|
|
@@ -192,11 +156,10 @@ export class SingleUserOAuthProvider {
|
|
|
192
156
|
if (!requestedScopes.every((scope) => record.scopes.includes(scope))) {
|
|
193
157
|
throw new AccessDeniedError("Refresh token cannot grant requested scopes");
|
|
194
158
|
}
|
|
195
|
-
this.
|
|
196
|
-
return this.issueTokens(client.client_id, requestedScopes, resource ?? record.resource);
|
|
159
|
+
return this.issueTokens(client.client_id, requestedScopes, resource ?? (record.resource ? new URL(record.resource) : undefined), refreshTokenHash);
|
|
197
160
|
}
|
|
198
161
|
async verifyAccessToken(token) {
|
|
199
|
-
const record = this.
|
|
162
|
+
const record = this.oauthStore.getAccessToken(hashToken(token));
|
|
200
163
|
if (!record || record.expiresAt < Math.floor(Date.now() / 1000)) {
|
|
201
164
|
throw new InvalidTokenError("Invalid or expired access token");
|
|
202
165
|
}
|
|
@@ -205,13 +168,16 @@ export class SingleUserOAuthProvider {
|
|
|
205
168
|
clientId: record.clientId,
|
|
206
169
|
scopes: record.scopes,
|
|
207
170
|
expiresAt: record.expiresAt,
|
|
208
|
-
resource: record.resource,
|
|
171
|
+
resource: record.resource ? new URL(record.resource) : undefined,
|
|
209
172
|
};
|
|
210
173
|
}
|
|
211
174
|
async revokeToken(_client, request) {
|
|
212
175
|
const hashed = hashToken(request.token);
|
|
213
|
-
this.
|
|
214
|
-
this.
|
|
176
|
+
this.oauthStore.deleteAccessToken(hashed);
|
|
177
|
+
this.oauthStore.deleteRefreshToken(hashed);
|
|
178
|
+
}
|
|
179
|
+
close() {
|
|
180
|
+
this.oauthStore.close();
|
|
215
181
|
}
|
|
216
182
|
validCodeRecord(client, authorizationCode) {
|
|
217
183
|
const record = this.codes.get(authorizationCode);
|
|
@@ -220,26 +186,31 @@ export class SingleUserOAuthProvider {
|
|
|
220
186
|
}
|
|
221
187
|
return record;
|
|
222
188
|
}
|
|
223
|
-
issueTokens(clientId, scopes, resource) {
|
|
189
|
+
issueTokens(clientId, scopes, resource, consumedRefreshTokenHash) {
|
|
224
190
|
const now = Math.floor(Date.now() / 1000);
|
|
225
191
|
const accessToken = randomToken();
|
|
226
192
|
const refreshToken = randomToken();
|
|
227
193
|
const accessExpiresAt = now + this.config.accessTokenTtlSeconds;
|
|
228
194
|
const refreshExpiresAt = now + this.config.refreshTokenTtlSeconds;
|
|
229
|
-
this.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
195
|
+
const saved = this.oauthStore.saveTokenPair({
|
|
196
|
+
accessTokenHash: hashToken(accessToken),
|
|
197
|
+
accessToken: {
|
|
198
|
+
clientId,
|
|
199
|
+
scopes,
|
|
200
|
+
expiresAt: accessExpiresAt,
|
|
201
|
+
resource: resource?.href,
|
|
202
|
+
},
|
|
203
|
+
refreshTokenHash: hashToken(refreshToken),
|
|
204
|
+
refreshToken: {
|
|
205
|
+
clientId,
|
|
206
|
+
scopes,
|
|
207
|
+
expiresAt: refreshExpiresAt,
|
|
208
|
+
resource: resource?.href,
|
|
209
|
+
},
|
|
210
|
+
}, consumedRefreshTokenHash);
|
|
211
|
+
if (!saved) {
|
|
212
|
+
throw new InvalidGrantError("Invalid refresh token");
|
|
213
|
+
}
|
|
243
214
|
return {
|
|
244
215
|
access_token: accessToken,
|
|
245
216
|
token_type: "bearer",
|