@waishnav/devspace 1.0.1 → 1.0.3
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/README.md +140 -30
- package/dist/apply-patch.js +399 -0
- package/dist/cli.js +18 -2
- package/dist/config.js +11 -13
- 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/process-platform.js +49 -0
- package/dist/process-sessions.js +329 -0
- package/dist/roots.js +5 -2
- package/dist/server.js +462 -203
- package/dist/skills.js +23 -3
- package/dist/ui/.vite/manifest.json +281 -281
- package/dist/ui/assets/{angular-html-B_77pPm_.js → angular-html-D6nc7PYw.js} +1 -1
- package/dist/ui/assets/{angular-ts-tGBi7sOr.js → angular-ts-BAIStLeI.js} +1 -1
- package/dist/ui/assets/{apl-CtVPpGgP.js → apl-CtEv5pop.js} +1 -1
- package/dist/ui/assets/{astro-CALPYvnG.js → astro-CVFIMXc6.js} +1 -1
- package/dist/ui/assets/{blade-Bh4eBXdk.js → blade-BgSlZ6jY.js} +1 -1
- package/dist/ui/assets/{c-BGkebjQE.js → c-bxLlCV26.js} +1 -1
- package/dist/ui/assets/{cobol-f7rP2PjR.js → cobol-BuZ-r51r.js} +1 -1
- package/dist/ui/assets/{coffee-E4OhMc7W.js → coffee-_JP0Joa7.js} +1 -1
- package/dist/ui/assets/{cpp-Cr_J6lB2.js → cpp-CP_eaz-H.js} +1 -1
- package/dist/ui/assets/{crystal-B_TFXhon.js → crystal-DZ-sEZd0.js} +1 -1
- package/dist/ui/assets/{css-DDMxq-4J.js → css-CZnyti9s.js} +1 -1
- package/dist/ui/assets/{edge-Bxqhgg5I.js → edge-DjsoDMTR.js} +1 -1
- package/dist/ui/assets/{elixir-WrSbgJJ1.js → elixir-C5b04AkY.js} +1 -1
- package/dist/ui/assets/{elm-BXBWJ4Kz.js → elm-Ddx-rgVY.js} +1 -1
- package/dist/ui/assets/{erb-D0iMw9rO.js → erb-cjF7oPcT.js} +1 -1
- package/dist/ui/assets/{git-rebase-BOrZ-_iR.js → git-rebase-DN5exBTl.js} +1 -1
- package/dist/ui/assets/{glimmer-js-PCWk96O8.js → glimmer-js-CEq0hNwa.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-4pYwX8pC.js → glimmer-ts-4uYGFkon.js} +1 -1
- package/dist/ui/assets/{glsl-CDyKZvZX.js → glsl-DEJ9tTWm.js} +1 -1
- package/dist/ui/assets/{graphql-8sIw92YT.js → graphql-Bim5WuQh.js} +1 -1
- package/dist/ui/assets/{hack-Ci96Zcew.js → hack-BcyVIXhE.js} +1 -1
- package/dist/ui/assets/{haml-4uzh6End.js → haml-D9YaukDg.js} +1 -1
- package/dist/ui/assets/{handlebars-B6Eq7QV8.js → handlebars-CFaMw9zH.js} +1 -1
- package/dist/ui/assets/{heavy-payload--bwRtRpo.js → heavy-payload-B9aTqgfs.js} +1 -1
- package/dist/ui/assets/{html-C29wL7bm.js → html-CgO_zx4D.js} +1 -1
- package/dist/ui/assets/{html-derivative-Cmd90ilz.js → html-derivative-BxqkysJT.js} +1 -1
- package/dist/ui/assets/{http-Bdr_R0Kw.js → http-CO5dv0RA.js} +1 -1
- package/dist/ui/assets/{hurl-CiTPn-zH.js → hurl-DRHtrK78.js} +1 -1
- package/dist/ui/assets/{java-C03kBuJH.js → java-BvHC_DgR.js} +1 -1
- package/dist/ui/assets/{javascript-135g9Wwx.js → javascript-C-3payQx.js} +1 -1
- package/dist/ui/assets/{jinja-CPpiEHqV.js → jinja-CAOPvEOH.js} +1 -1
- package/dist/ui/assets/{jison-qabCQzwp.js → jison-totCB1L-.js} +1 -1
- package/dist/ui/assets/{json-CtRj9Trp.js → json-D0J9rEe1.js} +1 -1
- package/dist/ui/assets/{jsx-Ccuy5Wm3.js → jsx-na-XQD_B.js} +1 -1
- package/dist/ui/assets/{julia-Fwya-MwM.js → julia-CqrTtZ4-.js} +1 -1
- package/dist/ui/assets/{just-c9YEH7Gf.js → just-pmrVSkMc.js} +1 -1
- package/dist/ui/assets/{latex-D9xoD6UX.js → latex-Cd_geX3E.js} +1 -1
- package/dist/ui/assets/{liquid-BKZLZSHN.js → liquid-C6ky5sJ-.js} +1 -1
- package/dist/ui/assets/{lua-CBD2zNyC.js → lua-dgyBbeof.js} +1 -1
- package/dist/ui/assets/{marko-Dkgf9bj3.js → marko-Dy5mfXY9.js} +1 -1
- package/dist/ui/assets/{mdc-BUgUKiX3.js → mdc-7NUvxgI1.js} +1 -1
- package/dist/ui/assets/{nginx-CAaav4Zh.js → nginx-C-KiEXbI.js} +1 -1
- package/dist/ui/assets/{nim-CTjWWztL.js → nim-BBw8kfl9.js} +1 -1
- package/dist/ui/assets/{perl-DTM5zgH4.js → perl-CyxhJgbO.js} +1 -1
- package/dist/ui/assets/{php-dqfFDbWm.js → php-m4LAOpam.js} +1 -1
- package/dist/ui/assets/{pug-4f_clTm2.js → pug-Cpl0baYI.js} +1 -1
- package/dist/ui/assets/{qml-BFeWjFRL.js → qml-BoSGNgIb.js} +1 -1
- package/dist/ui/assets/{r-ei1iNvHm.js → r-C0JhqQd5.js} +1 -1
- package/dist/ui/assets/{razor-jj95jncJ.js → razor-CntsBNoh.js} +1 -1
- package/dist/ui/assets/{regexp-0joUf3Cn.js → regexp-BNZmOCRl.js} +1 -1
- package/dist/ui/assets/{review-payload-DxI2fTnP.js → review-payload-BgdWHWiv.js} +1 -1
- package/dist/ui/assets/{rst-BMbcN8--.js → rst-BZ2XHIxM.js} +1 -1
- package/dist/ui/assets/{ruby-BQx0xtOB.js → ruby-BIthfCTP.js} +1 -1
- package/dist/ui/assets/{sas-C_Fl1EyP.js → sas-5aT3fOla.js} +1 -1
- package/dist/ui/assets/{scss-5vaLJPJl.js → scss-DZPfdkDy.js} +1 -1
- package/dist/ui/assets/{shellscript-p_y7YCYP.js → shellscript-DbhKc5Xj.js} +1 -1
- package/dist/ui/assets/{shellsession-ChZkMp2K.js → shellsession-X9-bcyHh.js} +1 -1
- package/dist/ui/assets/{soy-im25cA8U.js → soy-Bd4tecrW.js} +1 -1
- package/dist/ui/assets/{sql-BzvG6ssm.js → sql-Chf5JrDn.js} +1 -1
- package/dist/ui/assets/{stata-CwSzNM20.js → stata-DvMMIc_T.js} +1 -1
- package/dist/ui/assets/{surrealql-BWzKhI6S.js → surrealql-D5FMzVF5.js} +1 -1
- package/dist/ui/assets/{svelte-NMlQHjaa.js → svelte-CTiVXZrv.js} +1 -1
- package/dist/ui/assets/{templ-1W11KGSL.js → templ-ldnfq-7w.js} +1 -1
- package/dist/ui/assets/{tex-CTxN8q2c.js → tex-QW0_7T2r.js} +1 -1
- package/dist/ui/assets/{ts-tags-B6prphu-.js → ts-tags-CAKiWIBx.js} +1 -1
- package/dist/ui/assets/{tsx-Daks24y9.js → tsx-Bu9xJJmq.js} +1 -1
- package/dist/ui/assets/{twig-C0KeCxch.js → twig-Dp1752_V.js} +1 -1
- package/dist/ui/assets/{typescript-37-dkCwp.js → typescript-RfO03Kmm.js} +1 -1
- package/dist/ui/assets/{useFileDiffInstance--R0_ywL_.js → useFileDiffInstance-DjqU4y1a.js} +3 -3
- package/dist/ui/assets/{vue-Ba-v02vP.js → vue-Dlasv57G.js} +1 -1
- package/dist/ui/assets/{vue-html-D2UmbuN7.js → vue-html-BHuJ7anu.js} +1 -1
- package/dist/ui/assets/{vue-vine-ZftUW_ID.js → vue-vine-CavE0PSk.js} +1 -1
- package/dist/ui/assets/{workspace-app-CsMiGtz9.js → workspace-app-DDV_qdjx.js} +6 -6
- package/dist/ui/assets/workspace-app-DGD9R89D.css +1 -0
- package/dist/ui/assets/{xml-DrhRYxTS.js → xml-Bbv64l4W.js} +1 -1
- package/dist/ui/assets/{xsl-DvbIvBcQ.js → xsl-Sro4gyQY.js} +1 -1
- package/dist/ui/assets/{yaml-oktUYAzQ.js → yaml-Du68c8sA.js} +1 -1
- package/dist/ui/workspace-app.html +2 -2
- package/dist/workspace-store.js +0 -50
- package/docs/chatgpt-coding-workflow.md +24 -4
- package/docs/codex-tool-mode-qa.md +73 -0
- package/docs/configuration.md +28 -5
- package/docs/gotchas.md +11 -5
- package/docs/setup.md +1 -1
- package/package.json +10 -5
- package/scripts/fix-node-pty-permissions.mjs +22 -0
- package/dist/ui/assets/workspace-app-8--oTbCd.css +0 -1
package/dist/config.js
CHANGED
|
@@ -44,17 +44,16 @@ function normalizeAllowedHosts(rawHosts, derivedHosts) {
|
|
|
44
44
|
function parseBoolean(value) {
|
|
45
45
|
return ["1", "true", "yes", "on"].includes(value?.toLowerCase() ?? "");
|
|
46
46
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
function parseToolMode(env) {
|
|
48
|
+
const mode = env.DEVSPACE_TOOL_MODE;
|
|
49
|
+
if (mode === "minimal" || mode === "full" || mode === "codex")
|
|
50
|
+
return mode;
|
|
51
|
+
if (mode)
|
|
52
|
+
throw new Error(`Invalid DEVSPACE_TOOL_MODE: ${mode}`);
|
|
53
|
+
if (env.DEVSPACE_MINIMAL_TOOLS !== undefined) {
|
|
54
|
+
return parseBoolean(env.DEVSPACE_MINIMAL_TOOLS) ? "minimal" : "full";
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
return parseBoolean(env.DEVSPACE_MINIMAL_TOOLS);
|
|
57
|
-
return true;
|
|
56
|
+
return "minimal";
|
|
58
57
|
}
|
|
59
58
|
function parseLogLevel(value) {
|
|
60
59
|
if (!value || value === "info")
|
|
@@ -74,8 +73,7 @@ function parsePathList(value) {
|
|
|
74
73
|
return (value
|
|
75
74
|
?.split(",")
|
|
76
75
|
.map((entry) => entry.trim())
|
|
77
|
-
.filter(Boolean)
|
|
78
|
-
.map((entry) => resolve(expandHomePath(entry))) ?? []);
|
|
76
|
+
.filter(Boolean) ?? []);
|
|
79
77
|
}
|
|
80
78
|
function parseStringList(value, fallback) {
|
|
81
79
|
const entries = value
|
|
@@ -170,7 +168,7 @@ export function loadConfig(env = process.env) {
|
|
|
170
168
|
allowedRoots: parseAllowedRoots(env.DEVSPACE_ALLOWED_ROOTS ?? files.config.allowedRoots),
|
|
171
169
|
allowedHosts: parseAllowedHosts(env.DEVSPACE_ALLOWED_HOSTS, derivedAllowedHosts),
|
|
172
170
|
publicBaseUrl,
|
|
173
|
-
|
|
171
|
+
toolMode: parseToolMode(env),
|
|
174
172
|
toolNaming: parseToolNaming(env.DEVSPACE_TOOL_NAMING),
|
|
175
173
|
widgets: parseWidgetMode(env.DEVSPACE_WIDGETS),
|
|
176
174
|
stateDir: resolve(expandHomePath(env.DEVSPACE_STATE_DIR ?? files.config.stateDir ?? defaultStateDir())),
|
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",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { InvalidRequestError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
3
|
+
import { openDatabase } from "./db/client.js";
|
|
4
|
+
function redirectHostAllowed(redirectUri, allowedHosts) {
|
|
5
|
+
let parsed;
|
|
6
|
+
try {
|
|
7
|
+
parsed = new URL(redirectUri);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname))
|
|
13
|
+
return true;
|
|
14
|
+
return allowedHosts.includes(parsed.hostname);
|
|
15
|
+
}
|
|
16
|
+
export class SqliteOAuthStore {
|
|
17
|
+
database;
|
|
18
|
+
constructor(stateDir) {
|
|
19
|
+
this.database = openDatabase(stateDir);
|
|
20
|
+
this.deleteExpiredTokens(Math.floor(Date.now() / 1000));
|
|
21
|
+
}
|
|
22
|
+
getClient(clientId) {
|
|
23
|
+
const row = this.database.sqlite
|
|
24
|
+
.prepare("select client_json from oauth_clients where client_id = ?")
|
|
25
|
+
.get(clientId);
|
|
26
|
+
return row ? JSON.parse(row.client_json) : undefined;
|
|
27
|
+
}
|
|
28
|
+
registerClient(client, allowedRedirectHosts) {
|
|
29
|
+
if (!client.redirect_uris.every((uri) => redirectHostAllowed(String(uri), allowedRedirectHosts))) {
|
|
30
|
+
throw new InvalidRequestError("Client redirect_uri is not allowed for this DevSpace server");
|
|
31
|
+
}
|
|
32
|
+
const now = Math.floor(Date.now() / 1000);
|
|
33
|
+
const registered = {
|
|
34
|
+
...client,
|
|
35
|
+
client_id: `devspace-${randomUUID()}`,
|
|
36
|
+
client_id_issued_at: now,
|
|
37
|
+
token_endpoint_auth_method: client.token_endpoint_auth_method ?? "none",
|
|
38
|
+
grant_types: client.grant_types ?? ["authorization_code", "refresh_token"],
|
|
39
|
+
response_types: client.response_types ?? ["code"],
|
|
40
|
+
};
|
|
41
|
+
this.database.sqlite
|
|
42
|
+
.prepare("insert into oauth_clients (client_id, client_json, issued_at) values (?, ?, ?)")
|
|
43
|
+
.run(registered.client_id, JSON.stringify(registered), now);
|
|
44
|
+
return registered;
|
|
45
|
+
}
|
|
46
|
+
saveAccessToken(tokenHash, record) {
|
|
47
|
+
this.database.sqlite
|
|
48
|
+
.prepare(`insert into oauth_access_tokens (token_hash, client_id, scopes_json, expires_at, resource)
|
|
49
|
+
values (?, ?, ?, ?, ?)
|
|
50
|
+
on conflict(token_hash) do update set
|
|
51
|
+
client_id = excluded.client_id,
|
|
52
|
+
scopes_json = excluded.scopes_json,
|
|
53
|
+
expires_at = excluded.expires_at,
|
|
54
|
+
resource = excluded.resource`)
|
|
55
|
+
.run(tokenHash, record.clientId, JSON.stringify(record.scopes), record.expiresAt, record.resource ?? null);
|
|
56
|
+
}
|
|
57
|
+
getAccessToken(tokenHash) {
|
|
58
|
+
const row = this.database.sqlite
|
|
59
|
+
.prepare("select client_id, scopes_json, expires_at, resource from oauth_access_tokens where token_hash = ?")
|
|
60
|
+
.get(tokenHash);
|
|
61
|
+
return row ? rowToAccessTokenRecord(row) : undefined;
|
|
62
|
+
}
|
|
63
|
+
deleteAccessToken(tokenHash) {
|
|
64
|
+
this.database.sqlite.prepare("delete from oauth_access_tokens where token_hash = ?").run(tokenHash);
|
|
65
|
+
}
|
|
66
|
+
saveRefreshToken(tokenHash, record) {
|
|
67
|
+
this.database.sqlite
|
|
68
|
+
.prepare(`insert into oauth_refresh_tokens (token_hash, client_id, scopes_json, expires_at, resource)
|
|
69
|
+
values (?, ?, ?, ?, ?)
|
|
70
|
+
on conflict(token_hash) do update set
|
|
71
|
+
client_id = excluded.client_id,
|
|
72
|
+
scopes_json = excluded.scopes_json,
|
|
73
|
+
expires_at = excluded.expires_at,
|
|
74
|
+
resource = excluded.resource`)
|
|
75
|
+
.run(tokenHash, record.clientId, JSON.stringify(record.scopes), record.expiresAt, record.resource ?? null);
|
|
76
|
+
}
|
|
77
|
+
saveTokenPair(pair, consumedRefreshTokenHash) {
|
|
78
|
+
const save = this.database.sqlite.transaction(() => {
|
|
79
|
+
if (consumedRefreshTokenHash) {
|
|
80
|
+
const result = this.database.sqlite
|
|
81
|
+
.prepare("delete from oauth_refresh_tokens where token_hash = ?")
|
|
82
|
+
.run(consumedRefreshTokenHash);
|
|
83
|
+
if (result.changes !== 1)
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
this.saveAccessToken(pair.accessTokenHash, pair.accessToken);
|
|
87
|
+
this.saveRefreshToken(pair.refreshTokenHash, pair.refreshToken);
|
|
88
|
+
return true;
|
|
89
|
+
});
|
|
90
|
+
return save.immediate();
|
|
91
|
+
}
|
|
92
|
+
getRefreshToken(tokenHash) {
|
|
93
|
+
const row = this.database.sqlite
|
|
94
|
+
.prepare("select client_id, scopes_json, expires_at, resource from oauth_refresh_tokens where token_hash = ?")
|
|
95
|
+
.get(tokenHash);
|
|
96
|
+
return row ? rowToRefreshTokenRecord(row) : undefined;
|
|
97
|
+
}
|
|
98
|
+
deleteRefreshToken(tokenHash) {
|
|
99
|
+
this.database.sqlite.prepare("delete from oauth_refresh_tokens where token_hash = ?").run(tokenHash);
|
|
100
|
+
}
|
|
101
|
+
close() {
|
|
102
|
+
this.database.close();
|
|
103
|
+
}
|
|
104
|
+
deleteExpiredTokens(nowSeconds) {
|
|
105
|
+
this.database.sqlite.prepare("delete from oauth_access_tokens where expires_at < ?").run(nowSeconds);
|
|
106
|
+
this.database.sqlite.prepare("delete from oauth_refresh_tokens where expires_at < ?").run(nowSeconds);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export class SqliteOAuthClientsStore {
|
|
110
|
+
store;
|
|
111
|
+
allowedRedirectHosts;
|
|
112
|
+
constructor(store, allowedRedirectHosts) {
|
|
113
|
+
this.store = store;
|
|
114
|
+
this.allowedRedirectHosts = allowedRedirectHosts;
|
|
115
|
+
}
|
|
116
|
+
getClient(clientId) {
|
|
117
|
+
return this.store.getClient(clientId);
|
|
118
|
+
}
|
|
119
|
+
registerClient(client) {
|
|
120
|
+
return this.store.registerClient(client, this.allowedRedirectHosts);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function rowToAccessTokenRecord(row) {
|
|
124
|
+
return {
|
|
125
|
+
clientId: row.client_id,
|
|
126
|
+
scopes: JSON.parse(row.scopes_json),
|
|
127
|
+
expiresAt: row.expires_at,
|
|
128
|
+
resource: row.resource ?? undefined,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function rowToRefreshTokenRecord(row) {
|
|
132
|
+
return {
|
|
133
|
+
clientId: row.client_id,
|
|
134
|
+
scopes: JSON.parse(row.scopes_json),
|
|
135
|
+
expiresAt: row.expires_at,
|
|
136
|
+
resource: row.resource ?? undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
const defaultProcessTreeRuntime = {
|
|
4
|
+
platform: process.platform,
|
|
5
|
+
killGroup: (pid, signal) => process.kill(-pid, signal),
|
|
6
|
+
killWindowsTree: (pid) => {
|
|
7
|
+
const result = spawnSync("taskkill.exe", ["/pid", String(pid), "/T", "/F"], {
|
|
8
|
+
stdio: "ignore",
|
|
9
|
+
windowsHide: true,
|
|
10
|
+
});
|
|
11
|
+
return !result.error && result.status === 0;
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
const LOGIN_SHELLS = new Set(["bash", "ksh", "zsh"]);
|
|
15
|
+
const POSIX_SHELLS = new Set(["ash", "dash", "sh"]);
|
|
16
|
+
export function resolveShellCommand(command, platform = process.platform, environment = process.env) {
|
|
17
|
+
if (platform === "win32") {
|
|
18
|
+
return {
|
|
19
|
+
executable: environment.ComSpec ?? environment.COMSPEC ?? "cmd.exe",
|
|
20
|
+
args: ["/d", "/s", "/c", command],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const configuredShell = environment.SHELL;
|
|
24
|
+
const shellName = configuredShell ? basename(configuredShell) : "";
|
|
25
|
+
if (configuredShell && LOGIN_SHELLS.has(shellName)) {
|
|
26
|
+
return { executable: configuredShell, args: ["-lc", command] };
|
|
27
|
+
}
|
|
28
|
+
if (configuredShell && POSIX_SHELLS.has(shellName)) {
|
|
29
|
+
return { executable: configuredShell, args: ["-c", command] };
|
|
30
|
+
}
|
|
31
|
+
return { executable: "/bin/sh", args: ["-c", command] };
|
|
32
|
+
}
|
|
33
|
+
export function terminateProcessTree(child, signal, detached, runtime = defaultProcessTreeRuntime) {
|
|
34
|
+
if (runtime.platform === "win32" && child.pid) {
|
|
35
|
+
if (runtime.killWindowsTree(child.pid))
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
else if (detached && child.pid) {
|
|
39
|
+
try {
|
|
40
|
+
runtime.killGroup(child.pid, signal);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === "ESRCH")
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
child.kill(signal);
|
|
49
|
+
}
|