@moxxy/plugin-oauth 0.37.1 → 0.37.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.
@@ -2,8 +2,7 @@
2
2
  * Open a URL in the user's default browser. Platform-specific:
3
3
  * macOS → `open <url>`
4
4
  * Linux → `xdg-open <url>`
5
- * Win32 → `cmd /c start "" "<url>"` (the URL DOUBLE-QUOTED + args passed
6
- * verbatim — see {@link browserOpenCommand})
5
+ * Win32 → `explorer.exe <url>`
7
6
  *
8
7
  * Returns once the helper process is spawned — it does NOT wait for the
9
8
  * browser itself, since the user's flow continues asynchronously via
@@ -13,20 +12,16 @@
13
12
  export declare function openInBrowser(url: string): Promise<void>;
14
13
  /**
15
14
  * Resolve the platform's "open this URL in the default browser" command.
16
- * Exported + `platform`-parameterized so the Windows quoting can be unit-tested
15
+ * Exported + `platform`-parameterized so each platform can be unit-tested
17
16
  * without actually spawning a browser.
18
17
  *
19
- * Windows is the tricky one: `&` (and `|`, `<`, `>`, `^`) in a URL are cmd.exe
20
- * command separators. `cmd /c start "" http://a?x=1&y=2` truncates the URL at
21
- * the first `&`, so an OAuth authorize URL loses redirect_uri / code_challenge /
22
- * state and the provider rejects the request ("authorization error"). Wrapping
23
- * the URL in double quotes makes cmd treat it as one literal token; passing the
24
- * args verbatim (`windowsVerbatimArguments`) stops Node from re-quoting and
25
- * undoing that. The empty `""` is the required `start` window-title placeholder.
18
+ * Windows deliberately launches Explorer directly rather than interpolating a
19
+ * library-provided OAuth URL into `cmd.exe /c start`. `spawn` passes the URL as
20
+ * one argv entry, so `&`, `|`, `<`, `>` and `^` remain URL data rather than
21
+ * shell metacharacters.
26
22
  */
27
23
  export declare function browserOpenCommand(url: string, platform?: NodeJS.Platform): {
28
24
  cmd: string;
29
25
  args: string[];
30
- verbatim?: boolean;
31
26
  };
32
27
  //# sourceMappingURL=open-browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"open-browser.d.ts","sourceRoot":"","sources":["../src/open-browser.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwC9D;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAE,CAWrD"}
1
+ {"version":3,"file":"open-browser.d.ts","sourceRoot":"","sources":["../src/open-browser.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAqC9D;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAWjC"}
@@ -3,8 +3,7 @@ import { spawn } from 'node:child_process';
3
3
  * Open a URL in the user's default browser. Platform-specific:
4
4
  * macOS → `open <url>`
5
5
  * Linux → `xdg-open <url>`
6
- * Win32 → `cmd /c start "" "<url>"` (the URL DOUBLE-QUOTED + args passed
7
- * verbatim — see {@link browserOpenCommand})
6
+ * Win32 → `explorer.exe <url>`
8
7
  *
9
8
  * Returns once the helper process is spawned — it does NOT wait for the
10
9
  * browser itself, since the user's flow continues asynchronously via
@@ -12,14 +11,11 @@ import { spawn } from 'node:child_process';
12
11
  * so the caller can print it for the user to paste manually.
13
12
  */
14
13
  export async function openInBrowser(url) {
15
- const { cmd, args, verbatim } = browserOpenCommand(url);
14
+ const { cmd, args } = browserOpenCommand(url);
16
15
  await new Promise((resolve, reject) => {
17
16
  const child = spawn(cmd, args, {
18
17
  stdio: 'ignore',
19
18
  detached: true,
20
- // Windows only: keep cmd.exe from re-quoting our already-quoted URL,
21
- // which would otherwise break the `&` escaping (see below).
22
- ...(verbatim ? { windowsVerbatimArguments: true } : {}),
23
19
  });
24
20
  // Settle exactly once. An early spawn `error` rejects AND clears the pending
25
21
  // 50ms timer (else it would fire a stray resolve later and hold a live timer
@@ -56,26 +52,23 @@ export async function openInBrowser(url) {
56
52
  }
57
53
  /**
58
54
  * Resolve the platform's "open this URL in the default browser" command.
59
- * Exported + `platform`-parameterized so the Windows quoting can be unit-tested
55
+ * Exported + `platform`-parameterized so each platform can be unit-tested
60
56
  * without actually spawning a browser.
61
57
  *
62
- * Windows is the tricky one: `&` (and `|`, `<`, `>`, `^`) in a URL are cmd.exe
63
- * command separators. `cmd /c start "" http://a?x=1&y=2` truncates the URL at
64
- * the first `&`, so an OAuth authorize URL loses redirect_uri / code_challenge /
65
- * state and the provider rejects the request ("authorization error"). Wrapping
66
- * the URL in double quotes makes cmd treat it as one literal token; passing the
67
- * args verbatim (`windowsVerbatimArguments`) stops Node from re-quoting and
68
- * undoing that. The empty `""` is the required `start` window-title placeholder.
58
+ * Windows deliberately launches Explorer directly rather than interpolating a
59
+ * library-provided OAuth URL into `cmd.exe /c start`. `spawn` passes the URL as
60
+ * one argv entry, so `&`, `|`, `<`, `>` and `^` remain URL data rather than
61
+ * shell metacharacters.
69
62
  */
70
63
  export function browserOpenCommand(url, platform = process.platform) {
64
+ const parsed = new URL(url);
65
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
66
+ throw new Error(`refusing to open non-http OAuth URL: ${parsed.protocol}`);
67
+ }
71
68
  if (platform === 'darwin')
72
69
  return { cmd: 'open', args: [url] };
73
70
  if (platform === 'win32') {
74
- return {
75
- cmd: process.env.ComSpec || 'cmd.exe',
76
- args: ['/c', 'start', '""', `"${url}"`],
77
- verbatim: true,
78
- };
71
+ return { cmd: 'explorer.exe', args: [url] };
79
72
  }
80
73
  // Linux + everything else assumes a Freedesktop-compliant `xdg-open`.
81
74
  return { cmd: 'xdg-open', args: [url] };
@@ -1 +1 @@
1
- {"version":3,"file":"open-browser.js","sourceRoot":"","sources":["../src/open-browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW;IAC7C,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IACxD,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE;YAC7B,KAAK,EAAE,QAAQ;YACf,QAAQ,EAAE,IAAI;YACd,qEAAqE;YACrE,4DAA4D;YAC5D,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,wBAAwB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxD,CAAC,CAAC;QACH,6EAA6E;QAC7E,6EAA6E;QAC7E,4EAA4E;QAC5E,4EAA4E;QAC5E,6EAA6E;QAC7E,0EAA0E;QAC1E,6BAA6B;QAC7B,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,OAAO,GAAG,CAAC,GAAU,EAAQ,EAAE;YACnC,IAAI,OAAO;gBAAE,OAAO,CAAC,gDAAgD;YACrE,OAAO,GAAG,IAAI,CAAC;YACf,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAC,GAAG,CAAC,CAAC;QACd,CAAC,CAAC;QACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,qEAAqE;QACrE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QAChB,6EAA6E;QAC7E,8EAA8E;QAC9E,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC3B,+DAA+D;QAC/D,uCAAuC;QACvC,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,iEAAiE;QACjE,6DAA6D;IAC/D,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAW,EACX,WAA4B,OAAO,CAAC,QAAQ;IAE5C,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/D,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO;YACL,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,IAAI,SAAS;YACrC,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,GAAG,CAAC;YACvC,QAAQ,EAAE,IAAI;SACf,CAAC;IACJ,CAAC;IACD,sEAAsE;IACtE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;AAC1C,CAAC"}
1
+ {"version":3,"file":"open-browser.js","sourceRoot":"","sources":["../src/open-browser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW;IAC7C,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAC9C,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC1C,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE;YAC7B,KAAK,EAAE,QAAQ;YACf,QAAQ,EAAE,IAAI;SACf,CAAC,CAAC;QACH,6EAA6E;QAC7E,6EAA6E;QAC7E,4EAA4E;QAC5E,4EAA4E;QAC5E,6EAA6E;QAC7E,0EAA0E;QAC1E,6BAA6B;QAC7B,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,OAAO,GAAG,CAAC,GAAU,EAAQ,EAAE;YACnC,IAAI,OAAO;gBAAE,OAAO,CAAC,gDAAgD;YACrE,OAAO,GAAG,IAAI,CAAC;YACf,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,CAAC,GAAG,CAAC,CAAC;QACd,CAAC,CAAC;QACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,OAAO,EAAE,CAAC;QACZ,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,qEAAqE;QACrE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QAChB,6EAA6E;QAC7E,8EAA8E;QAC9E,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QAC3B,+DAA+D;QAC/D,uCAAuC;QACvC,KAAK,CAAC,KAAK,EAAE,CAAC;QACd,iEAAiE;QACjE,6DAA6D;IAC/D,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB,CAChC,GAAW,EACX,WAA4B,OAAO,CAAC,QAAQ;IAE5C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CAAC,wCAAwC,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;IAC/D,IAAI,QAAQ,KAAK,OAAO,EAAE,CAAC;QACzB,OAAO,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;IAC9C,CAAC;IACD,sEAAsE;IACtE,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;AAC1C,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moxxy/plugin-oauth",
3
- "version": "0.37.1",
3
+ "version": "0.37.2",
4
4
  "description": "Generic OAuth 2.0 + PKCE client. Loopback callback server, vault-backed token storage, refresh helpers. Bundled skills for Google Workspace + arbitrary providers.",
5
5
  "keywords": [
6
6
  "moxxy",
@@ -54,15 +54,15 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "zod": "^3.25.76",
57
- "@moxxy/plugin-vault": "0.37.1",
58
- "@moxxy/sdk": "0.37.1"
57
+ "@moxxy/sdk": "0.37.2",
58
+ "@moxxy/plugin-vault": "0.37.2"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/node": "^22.10.0",
62
62
  "typescript": "^5.7.3",
63
63
  "vitest": "^3.2.6",
64
- "@moxxy/vitest-preset": "0.0.0",
65
- "@moxxy/tsconfig": "0.0.0"
64
+ "@moxxy/tsconfig": "0.0.0",
65
+ "@moxxy/vitest-preset": "0.0.0"
66
66
  },
67
67
  "scripts": {
68
68
  "build": "tsc -p tsconfig.json",
@@ -1,8 +1,8 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { browserOpenCommand } from './open-browser';
3
3
 
4
- // A realistic OAuth authorize URL: many `&`-separated params, which cmd.exe
5
- // treats as command separators unless the whole URL is quoted.
4
+ // A realistic OAuth authorize URL with shell metacharacters that must remain a
5
+ // single argv value on every platform.
6
6
  const AUTH_URL =
7
7
  'https://auth.openai.com/oauth/authorize?client_id=app_x&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&response_type=code&scope=openid+profile&code_challenge=abc123&state=deadbeef';
8
8
 
@@ -15,16 +15,16 @@ describe('browserOpenCommand', () => {
15
15
  expect(browserOpenCommand(AUTH_URL, 'linux')).toEqual({ cmd: 'xdg-open', args: [AUTH_URL] });
16
16
  });
17
17
 
18
- it('Windows DOUBLE-QUOTES the URL so cmd.exe does not split it on `&`', () => {
19
- const { cmd, args, verbatim } = browserOpenCommand(AUTH_URL, 'win32');
20
- expect(cmd).toMatch(/cmd(\.exe)?$/i);
21
- expect(verbatim).toBe(true);
22
- expect(args.slice(0, 3)).toEqual(['/c', 'start', '""']);
23
- // The whole URL — every `&` included — is inside one double-quoted token.
24
- expect(args[3]).toBe(`"${AUTH_URL}"`);
25
- // Sanity: the quoted arg still contains all the params after the first `&`.
26
- expect(args[3]).toContain('redirect_uri');
27
- expect(args[3]).toContain('code_challenge');
28
- expect(args[3]).toContain('state=deadbeef');
18
+ it('Windows bypasses the shell and passes the complete URL to Explorer', () => {
19
+ expect(browserOpenCommand(AUTH_URL, 'win32')).toEqual({
20
+ cmd: 'explorer.exe',
21
+ args: [AUTH_URL],
22
+ });
23
+ });
24
+
25
+ it('rejects non-web schemes and switch-shaped input before spawning', () => {
26
+ expect(() => browserOpenCommand('file:///tmp/token', 'win32')).toThrow('non-http');
27
+ expect(() => browserOpenCommand('javascript:alert(1)', 'win32')).toThrow('non-http');
28
+ expect(() => browserOpenCommand('/select,C:\\secrets', 'win32')).toThrow();
29
29
  });
30
30
  });
@@ -4,8 +4,7 @@ import { spawn } from 'node:child_process';
4
4
  * Open a URL in the user's default browser. Platform-specific:
5
5
  * macOS → `open <url>`
6
6
  * Linux → `xdg-open <url>`
7
- * Win32 → `cmd /c start "" "<url>"` (the URL DOUBLE-QUOTED + args passed
8
- * verbatim — see {@link browserOpenCommand})
7
+ * Win32 → `explorer.exe <url>`
9
8
  *
10
9
  * Returns once the helper process is spawned — it does NOT wait for the
11
10
  * browser itself, since the user's flow continues asynchronously via
@@ -13,14 +12,11 @@ import { spawn } from 'node:child_process';
13
12
  * so the caller can print it for the user to paste manually.
14
13
  */
15
14
  export async function openInBrowser(url: string): Promise<void> {
16
- const { cmd, args, verbatim } = browserOpenCommand(url);
15
+ const { cmd, args } = browserOpenCommand(url);
17
16
  await new Promise<void>((resolve, reject) => {
18
17
  const child = spawn(cmd, args, {
19
18
  stdio: 'ignore',
20
19
  detached: true,
21
- // Windows only: keep cmd.exe from re-quoting our already-quoted URL,
22
- // which would otherwise break the `&` escaping (see below).
23
- ...(verbatim ? { windowsVerbatimArguments: true } : {}),
24
20
  });
25
21
  // Settle exactly once. An early spawn `error` rejects AND clears the pending
26
22
  // 50ms timer (else it would fire a stray resolve later and hold a live timer
@@ -56,28 +52,25 @@ export async function openInBrowser(url: string): Promise<void> {
56
52
 
57
53
  /**
58
54
  * Resolve the platform's "open this URL in the default browser" command.
59
- * Exported + `platform`-parameterized so the Windows quoting can be unit-tested
55
+ * Exported + `platform`-parameterized so each platform can be unit-tested
60
56
  * without actually spawning a browser.
61
57
  *
62
- * Windows is the tricky one: `&` (and `|`, `<`, `>`, `^`) in a URL are cmd.exe
63
- * command separators. `cmd /c start "" http://a?x=1&y=2` truncates the URL at
64
- * the first `&`, so an OAuth authorize URL loses redirect_uri / code_challenge /
65
- * state and the provider rejects the request ("authorization error"). Wrapping
66
- * the URL in double quotes makes cmd treat it as one literal token; passing the
67
- * args verbatim (`windowsVerbatimArguments`) stops Node from re-quoting and
68
- * undoing that. The empty `""` is the required `start` window-title placeholder.
58
+ * Windows deliberately launches Explorer directly rather than interpolating a
59
+ * library-provided OAuth URL into `cmd.exe /c start`. `spawn` passes the URL as
60
+ * one argv entry, so `&`, `|`, `<`, `>` and `^` remain URL data rather than
61
+ * shell metacharacters.
69
62
  */
70
63
  export function browserOpenCommand(
71
64
  url: string,
72
65
  platform: NodeJS.Platform = process.platform,
73
- ): { cmd: string; args: string[]; verbatim?: boolean } {
66
+ ): { cmd: string; args: string[] } {
67
+ const parsed = new URL(url);
68
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
69
+ throw new Error(`refusing to open non-http OAuth URL: ${parsed.protocol}`);
70
+ }
74
71
  if (platform === 'darwin') return { cmd: 'open', args: [url] };
75
72
  if (platform === 'win32') {
76
- return {
77
- cmd: process.env.ComSpec || 'cmd.exe',
78
- args: ['/c', 'start', '""', `"${url}"`],
79
- verbatim: true,
80
- };
73
+ return { cmd: 'explorer.exe', args: [url] };
81
74
  }
82
75
  // Linux + everything else assumes a Freedesktop-compliant `xdg-open`.
83
76
  return { cmd: 'xdg-open', args: [url] };