@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.
Files changed (102) hide show
  1. package/README.md +140 -30
  2. package/dist/apply-patch.js +399 -0
  3. package/dist/cli.js +18 -2
  4. package/dist/config.js +11 -13
  5. package/dist/db/client.js +10 -3
  6. package/dist/db/migrations.js +123 -0
  7. package/dist/db/schema.js +24 -1
  8. package/dist/oauth-provider.js +35 -64
  9. package/dist/oauth-store.js +138 -0
  10. package/dist/process-platform.js +49 -0
  11. package/dist/process-sessions.js +329 -0
  12. package/dist/roots.js +5 -2
  13. package/dist/server.js +462 -203
  14. package/dist/skills.js +23 -3
  15. package/dist/ui/.vite/manifest.json +281 -281
  16. package/dist/ui/assets/{angular-html-B_77pPm_.js → angular-html-D6nc7PYw.js} +1 -1
  17. package/dist/ui/assets/{angular-ts-tGBi7sOr.js → angular-ts-BAIStLeI.js} +1 -1
  18. package/dist/ui/assets/{apl-CtVPpGgP.js → apl-CtEv5pop.js} +1 -1
  19. package/dist/ui/assets/{astro-CALPYvnG.js → astro-CVFIMXc6.js} +1 -1
  20. package/dist/ui/assets/{blade-Bh4eBXdk.js → blade-BgSlZ6jY.js} +1 -1
  21. package/dist/ui/assets/{c-BGkebjQE.js → c-bxLlCV26.js} +1 -1
  22. package/dist/ui/assets/{cobol-f7rP2PjR.js → cobol-BuZ-r51r.js} +1 -1
  23. package/dist/ui/assets/{coffee-E4OhMc7W.js → coffee-_JP0Joa7.js} +1 -1
  24. package/dist/ui/assets/{cpp-Cr_J6lB2.js → cpp-CP_eaz-H.js} +1 -1
  25. package/dist/ui/assets/{crystal-B_TFXhon.js → crystal-DZ-sEZd0.js} +1 -1
  26. package/dist/ui/assets/{css-DDMxq-4J.js → css-CZnyti9s.js} +1 -1
  27. package/dist/ui/assets/{edge-Bxqhgg5I.js → edge-DjsoDMTR.js} +1 -1
  28. package/dist/ui/assets/{elixir-WrSbgJJ1.js → elixir-C5b04AkY.js} +1 -1
  29. package/dist/ui/assets/{elm-BXBWJ4Kz.js → elm-Ddx-rgVY.js} +1 -1
  30. package/dist/ui/assets/{erb-D0iMw9rO.js → erb-cjF7oPcT.js} +1 -1
  31. package/dist/ui/assets/{git-rebase-BOrZ-_iR.js → git-rebase-DN5exBTl.js} +1 -1
  32. package/dist/ui/assets/{glimmer-js-PCWk96O8.js → glimmer-js-CEq0hNwa.js} +1 -1
  33. package/dist/ui/assets/{glimmer-ts-4pYwX8pC.js → glimmer-ts-4uYGFkon.js} +1 -1
  34. package/dist/ui/assets/{glsl-CDyKZvZX.js → glsl-DEJ9tTWm.js} +1 -1
  35. package/dist/ui/assets/{graphql-8sIw92YT.js → graphql-Bim5WuQh.js} +1 -1
  36. package/dist/ui/assets/{hack-Ci96Zcew.js → hack-BcyVIXhE.js} +1 -1
  37. package/dist/ui/assets/{haml-4uzh6End.js → haml-D9YaukDg.js} +1 -1
  38. package/dist/ui/assets/{handlebars-B6Eq7QV8.js → handlebars-CFaMw9zH.js} +1 -1
  39. package/dist/ui/assets/{heavy-payload--bwRtRpo.js → heavy-payload-B9aTqgfs.js} +1 -1
  40. package/dist/ui/assets/{html-C29wL7bm.js → html-CgO_zx4D.js} +1 -1
  41. package/dist/ui/assets/{html-derivative-Cmd90ilz.js → html-derivative-BxqkysJT.js} +1 -1
  42. package/dist/ui/assets/{http-Bdr_R0Kw.js → http-CO5dv0RA.js} +1 -1
  43. package/dist/ui/assets/{hurl-CiTPn-zH.js → hurl-DRHtrK78.js} +1 -1
  44. package/dist/ui/assets/{java-C03kBuJH.js → java-BvHC_DgR.js} +1 -1
  45. package/dist/ui/assets/{javascript-135g9Wwx.js → javascript-C-3payQx.js} +1 -1
  46. package/dist/ui/assets/{jinja-CPpiEHqV.js → jinja-CAOPvEOH.js} +1 -1
  47. package/dist/ui/assets/{jison-qabCQzwp.js → jison-totCB1L-.js} +1 -1
  48. package/dist/ui/assets/{json-CtRj9Trp.js → json-D0J9rEe1.js} +1 -1
  49. package/dist/ui/assets/{jsx-Ccuy5Wm3.js → jsx-na-XQD_B.js} +1 -1
  50. package/dist/ui/assets/{julia-Fwya-MwM.js → julia-CqrTtZ4-.js} +1 -1
  51. package/dist/ui/assets/{just-c9YEH7Gf.js → just-pmrVSkMc.js} +1 -1
  52. package/dist/ui/assets/{latex-D9xoD6UX.js → latex-Cd_geX3E.js} +1 -1
  53. package/dist/ui/assets/{liquid-BKZLZSHN.js → liquid-C6ky5sJ-.js} +1 -1
  54. package/dist/ui/assets/{lua-CBD2zNyC.js → lua-dgyBbeof.js} +1 -1
  55. package/dist/ui/assets/{marko-Dkgf9bj3.js → marko-Dy5mfXY9.js} +1 -1
  56. package/dist/ui/assets/{mdc-BUgUKiX3.js → mdc-7NUvxgI1.js} +1 -1
  57. package/dist/ui/assets/{nginx-CAaav4Zh.js → nginx-C-KiEXbI.js} +1 -1
  58. package/dist/ui/assets/{nim-CTjWWztL.js → nim-BBw8kfl9.js} +1 -1
  59. package/dist/ui/assets/{perl-DTM5zgH4.js → perl-CyxhJgbO.js} +1 -1
  60. package/dist/ui/assets/{php-dqfFDbWm.js → php-m4LAOpam.js} +1 -1
  61. package/dist/ui/assets/{pug-4f_clTm2.js → pug-Cpl0baYI.js} +1 -1
  62. package/dist/ui/assets/{qml-BFeWjFRL.js → qml-BoSGNgIb.js} +1 -1
  63. package/dist/ui/assets/{r-ei1iNvHm.js → r-C0JhqQd5.js} +1 -1
  64. package/dist/ui/assets/{razor-jj95jncJ.js → razor-CntsBNoh.js} +1 -1
  65. package/dist/ui/assets/{regexp-0joUf3Cn.js → regexp-BNZmOCRl.js} +1 -1
  66. package/dist/ui/assets/{review-payload-DxI2fTnP.js → review-payload-BgdWHWiv.js} +1 -1
  67. package/dist/ui/assets/{rst-BMbcN8--.js → rst-BZ2XHIxM.js} +1 -1
  68. package/dist/ui/assets/{ruby-BQx0xtOB.js → ruby-BIthfCTP.js} +1 -1
  69. package/dist/ui/assets/{sas-C_Fl1EyP.js → sas-5aT3fOla.js} +1 -1
  70. package/dist/ui/assets/{scss-5vaLJPJl.js → scss-DZPfdkDy.js} +1 -1
  71. package/dist/ui/assets/{shellscript-p_y7YCYP.js → shellscript-DbhKc5Xj.js} +1 -1
  72. package/dist/ui/assets/{shellsession-ChZkMp2K.js → shellsession-X9-bcyHh.js} +1 -1
  73. package/dist/ui/assets/{soy-im25cA8U.js → soy-Bd4tecrW.js} +1 -1
  74. package/dist/ui/assets/{sql-BzvG6ssm.js → sql-Chf5JrDn.js} +1 -1
  75. package/dist/ui/assets/{stata-CwSzNM20.js → stata-DvMMIc_T.js} +1 -1
  76. package/dist/ui/assets/{surrealql-BWzKhI6S.js → surrealql-D5FMzVF5.js} +1 -1
  77. package/dist/ui/assets/{svelte-NMlQHjaa.js → svelte-CTiVXZrv.js} +1 -1
  78. package/dist/ui/assets/{templ-1W11KGSL.js → templ-ldnfq-7w.js} +1 -1
  79. package/dist/ui/assets/{tex-CTxN8q2c.js → tex-QW0_7T2r.js} +1 -1
  80. package/dist/ui/assets/{ts-tags-B6prphu-.js → ts-tags-CAKiWIBx.js} +1 -1
  81. package/dist/ui/assets/{tsx-Daks24y9.js → tsx-Bu9xJJmq.js} +1 -1
  82. package/dist/ui/assets/{twig-C0KeCxch.js → twig-Dp1752_V.js} +1 -1
  83. package/dist/ui/assets/{typescript-37-dkCwp.js → typescript-RfO03Kmm.js} +1 -1
  84. package/dist/ui/assets/{useFileDiffInstance--R0_ywL_.js → useFileDiffInstance-DjqU4y1a.js} +3 -3
  85. package/dist/ui/assets/{vue-Ba-v02vP.js → vue-Dlasv57G.js} +1 -1
  86. package/dist/ui/assets/{vue-html-D2UmbuN7.js → vue-html-BHuJ7anu.js} +1 -1
  87. package/dist/ui/assets/{vue-vine-ZftUW_ID.js → vue-vine-CavE0PSk.js} +1 -1
  88. package/dist/ui/assets/{workspace-app-CsMiGtz9.js → workspace-app-DDV_qdjx.js} +6 -6
  89. package/dist/ui/assets/workspace-app-DGD9R89D.css +1 -0
  90. package/dist/ui/assets/{xml-DrhRYxTS.js → xml-Bbv64l4W.js} +1 -1
  91. package/dist/ui/assets/{xsl-DvbIvBcQ.js → xsl-Sro4gyQY.js} +1 -1
  92. package/dist/ui/assets/{yaml-oktUYAzQ.js → yaml-Du68c8sA.js} +1 -1
  93. package/dist/ui/workspace-app.html +2 -2
  94. package/dist/workspace-store.js +0 -50
  95. package/docs/chatgpt-coding-workflow.md +24 -4
  96. package/docs/codex-tool-mode-qa.md +73 -0
  97. package/docs/configuration.md +28 -5
  98. package/docs/gotchas.md +11 -5
  99. package/docs/setup.md +1 -1
  100. package/package.json +10 -5
  101. package/scripts/fix-node-pty-permissions.mjs +22 -0
  102. 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 parseMinimalTools(env) {
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}`);
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
- if (env.DEVSPACE_MINIMAL_TOOLS !== undefined)
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
- minimalTools: parseMinimalTools(env),
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
- const sqlite = new Database(databasePath(stateDir));
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
+ });
@@ -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
- accessTokens = new Map();
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.clientsStore = new InMemoryOAuthClientsStore(config.allowedRedirectHosts);
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 record = this.refreshTokens.get(hashToken(refreshToken));
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.refreshTokens.delete(hashToken(refreshToken));
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.accessTokens.get(hashToken(token));
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.accessTokens.delete(hashed);
214
- this.refreshTokens.delete(hashed);
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.accessTokens.set(hashToken(accessToken), {
230
- token: accessToken,
231
- clientId,
232
- scopes,
233
- expiresAt: accessExpiresAt,
234
- resource,
235
- });
236
- this.refreshTokens.set(hashToken(refreshToken), {
237
- token: refreshToken,
238
- clientId,
239
- scopes,
240
- expiresAt: refreshExpiresAt,
241
- resource,
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
+ }