@aemeath/surf-cli 0.1.0

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +102 -0
  3. package/SECURITY.md +21 -0
  4. package/dist/adapters/credential-store.d.ts +26 -0
  5. package/dist/adapters/credential-store.js +69 -0
  6. package/dist/adapters/credential-store.js.map +1 -0
  7. package/dist/adapters/openvpn-runner.d.ts +56 -0
  8. package/dist/adapters/openvpn-runner.js +413 -0
  9. package/dist/adapters/openvpn-runner.js.map +1 -0
  10. package/dist/adapters/state-store.d.ts +10 -0
  11. package/dist/adapters/state-store.js +38 -0
  12. package/dist/adapters/state-store.js.map +1 -0
  13. package/dist/adapters/surfshark-account-client.d.ts +31 -0
  14. package/dist/adapters/surfshark-account-client.js +226 -0
  15. package/dist/adapters/surfshark-account-client.js.map +1 -0
  16. package/dist/adapters/surfshark-config-client.d.ts +6 -0
  17. package/dist/adapters/surfshark-config-client.js +29 -0
  18. package/dist/adapters/surfshark-config-client.js.map +1 -0
  19. package/dist/adapters/xdg-paths.d.ts +14 -0
  20. package/dist/adapters/xdg-paths.js +46 -0
  21. package/dist/adapters/xdg-paths.js.map +1 -0
  22. package/dist/cli.d.ts +3 -0
  23. package/dist/cli.js +81 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/commands/auth.d.ts +3 -0
  26. package/dist/commands/auth.js +118 -0
  27. package/dist/commands/auth.js.map +1 -0
  28. package/dist/commands/doctor.d.ts +3 -0
  29. package/dist/commands/doctor.js +21 -0
  30. package/dist/commands/doctor.js.map +1 -0
  31. package/dist/commands/prompt.d.ts +4 -0
  32. package/dist/commands/prompt.js +35 -0
  33. package/dist/commands/prompt.js.map +1 -0
  34. package/dist/commands/servers.d.ts +3 -0
  35. package/dist/commands/servers.js +38 -0
  36. package/dist/commands/servers.js.map +1 -0
  37. package/dist/commands/vpn.d.ts +7 -0
  38. package/dist/commands/vpn.js +137 -0
  39. package/dist/commands/vpn.js.map +1 -0
  40. package/dist/core/errors.d.ts +12 -0
  41. package/dist/core/errors.js +37 -0
  42. package/dist/core/errors.js.map +1 -0
  43. package/dist/core/types.d.ts +56 -0
  44. package/dist/core/types.js +29 -0
  45. package/dist/core/types.js.map +1 -0
  46. package/dist/services/auth-service.d.ts +16 -0
  47. package/dist/services/auth-service.js +49 -0
  48. package/dist/services/auth-service.js.map +1 -0
  49. package/dist/services/doctor-service.d.ts +11 -0
  50. package/dist/services/doctor-service.js +61 -0
  51. package/dist/services/doctor-service.js.map +1 -0
  52. package/dist/services/server-catalog-service.d.ts +17 -0
  53. package/dist/services/server-catalog-service.js +112 -0
  54. package/dist/services/server-catalog-service.js.map +1 -0
  55. package/dist/services/vpn-service.d.ts +26 -0
  56. package/dist/services/vpn-service.js +133 -0
  57. package/dist/services/vpn-service.js.map +1 -0
  58. package/package.json +69 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SolarisCode
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 ADDED
@@ -0,0 +1,102 @@
1
+ # 🌊 surf-cli
2
+
3
+ `surf-cli` は、Surfshark の OpenVPN 手動設定を CLI から扱うための TypeScript 製ツールです。Linux を主対象にし、`openvpn` が導入済みであることを前提に `sudo openvpn` を起動します。macOS は `openvpn` CLI がある場合のみ best-effort です。
4
+
5
+ > 注意: Surfshark 公式ドキュメントでは、OpenVPN 手動接続で使う username/password は通常のアカウントメール・パスワードとは別物です。通常ログイン API は公開仕様ではないため、メール/パスワード・コードログインは experimental とし、失敗時は手動 OpenVPN 認証情報入力へフォールバックします。
6
+
7
+ ## 📦 インストール
8
+
9
+ ### パッケージマネージャー
10
+
11
+ ```bash
12
+ npm install -g @aemeath/surf-cli
13
+ # bun install -g @aemeath/surf-cli
14
+ # pnpm install -g @aemeath/surf-cli
15
+ # yarn global add @aemeath/surf-cli
16
+ ```
17
+
18
+ ### インストールスクリプト
19
+
20
+ ```bash
21
+ curl -fsSL https://raw.githubusercontent.com/aemeath/surf-cli/master/scripts/install.sh | bash
22
+ ```
23
+
24
+ ### 開発版(ソースから)
25
+
26
+ ```bash
27
+ git clone https://codeberg.org/aemeath/surf-cli.git
28
+ cd surf-cli
29
+ corepack enable
30
+ pnpm install
31
+ pnpm build
32
+ pnpm link --global
33
+ ```
34
+
35
+ ## ✅ 要件
36
+
37
+ - Node.js 22 以上
38
+ - Linux(推奨)または macOS(best-effort)
39
+ - `openvpn`
40
+ - `sudo`
41
+ - Surfshark の有効な契約
42
+
43
+ ## 🚀 使い方
44
+
45
+ ### Quick Start
46
+
47
+ ```bash
48
+ surf-cli doctor # 前提条件を確認
49
+ surf-cli auth login --manual # OpenVPN 認証情報を登録
50
+ surf-cli servers refresh # サーバー設定をダウンロード
51
+ surf-cli connect jp-tok # 日本・東京サーバーに接続
52
+ surf-cli status # 接続状態を確認
53
+ surf-cli disconnect # 切断
54
+ ```
55
+
56
+ ヘルプは `surf-cli --help` または各コマンドで `surf-cli <command> --help` で確認できます。
57
+
58
+ ## 🔐 認証
59
+
60
+ 推奨は Surfshark dashboard の `VPN > Manual Setup > Desktop or mobile > OpenVPN` で確認できる OpenVPN service credentials の手動登録です。
61
+
62
+ ```bash
63
+ surf-cli auth login --manual
64
+ ```
65
+
66
+ experimental なログイン経路(`--email`, `--code`)も用意していますが、Surfshark の非公開 Web API に依存するため、OpenVPN service credentials を取得できない場合は手動入力へフォールバックします。
67
+
68
+ 認証情報は OS keyring(`@napi-rs/keyring`)へ保存します。keyring が使えない環境では、平文ファイルへ自動保存しません。
69
+
70
+ 詳細は [`docs/topics/authentication.md`](docs/topics/authentication.md) を参照してください。
71
+
72
+ ## 🔧 トラブルシュート
73
+
74
+ `CONNECTION_FAILED` で OpenVPN が初期化前に終了した場合、`surf-cli` は OpenVPN log、起動直後の stderr/stdout、終了コードの順に診断情報を表示します。
75
+
76
+ - まず `surf-cli doctor` で `openvpn` と `sudo` が利用できるか確認してください。
77
+ - sudo password prompt が出る場合は、端末でパスワードを入力してください。
78
+ - 詳細を直接確認したい場合は `surf-cli connect jp-tok --foreground` のように foreground 実行してください。
79
+ - `AUTH_FAILED` の場合は、通常の Surfshark アカウントではなく OpenVPN service credentials を `surf-cli auth login --manual` で登録し直してください。
80
+
81
+ 詳細は [`docs/topics/vpn-connection.md`](docs/topics/vpn-connection.md) を参照してください。
82
+
83
+ ## 📚 ドキュメント
84
+
85
+ | ドキュメント | 内容 |
86
+ | ---------------------------------------------------------------- | -------------------------------------------- |
87
+ | [`docs/README.md`](docs/README.md) | ドキュメントインデックス・アーキテクチャ概要 |
88
+ | [`docs/topics/authentication.md`](docs/topics/authentication.md) | 認証方法の詳細 |
89
+ | [`docs/topics/vpn-connection.md`](docs/topics/vpn-connection.md) | VPN 接続モデルの詳細 |
90
+ | [`docs/topics/server-catalog.md`](docs/topics/server-catalog.md) | サーバー設定の取得・展開・検索 |
91
+ | [`docs/topics/data-storage.md`](docs/topics/data-storage.md) | データ保存場所・XDG パス・権限 |
92
+ | [`docs/topics/error-handling.md`](docs/topics/error-handling.md) | エラー種別・exit code |
93
+ | [`docs/development.md`](docs/development.md) | 開発手順・CI/CD |
94
+
95
+ ## 🔒 セキュリティ
96
+
97
+ - Surfshark アカウントパスワードは保存しません。
98
+ - OpenVPN service credentials は OS keyring に保存します。
99
+ - 実行時 auth file は XDG runtime 配下に 0600 で作成します。
100
+ - CI に Surfshark credentials を渡す設計にはしていません。
101
+
102
+ 脆弱性の報告は [`SECURITY.md`](SECURITY.md) を参照してください。
package/SECURITY.md ADDED
@@ -0,0 +1,21 @@
1
+ # Security Policy
2
+
3
+ ## 対象
4
+
5
+ `surf-cli` は Surfshark OpenVPN 手動設定をローカル端末で扱う CLI です。現在の主対象は Linux です。
6
+
7
+ ## 機密情報の扱い
8
+
9
+ - Surfshark アカウントメール/パスワードは永続化しません。
10
+ - OpenVPN service credentials は OS keyring に保存します。
11
+ - keyring が使えない場合、平文ファイルへの自動保存は行いません。
12
+ - OpenVPN 実行時のみ `$XDG_RUNTIME_DIR/surf-cli/openvpn.auth` を 0600 で作成します。
13
+ - ログとエラーメッセージには password/token を出さない方針です。
14
+
15
+ ## 非公開 API について
16
+
17
+ メール/パスワード・コードログインは Surfshark の公開仕様ではない Web API に依存する experimental 機能です。仕様変更や 2FA/Cloudflare challenge により失敗する可能性があります。安定運用には `surf-cli auth login --manual` を使ってください。
18
+
19
+ ## 報告
20
+
21
+ 脆弱性を見つけた場合は、公開 issue に秘密情報を含めず、Codeberg の issue で概要のみを共有してください。認証情報、ログ、`.ovpn` 以外の個人情報、トークンは貼り付けないでください。
@@ -0,0 +1,26 @@
1
+ import { type OpenVpnCredentials } from '../core/types.js';
2
+ export interface CredentialStoreStatus {
3
+ available: boolean;
4
+ backend: string;
5
+ message?: string;
6
+ }
7
+ export interface CredentialStore {
8
+ get(): Promise<OpenVpnCredentials | null>;
9
+ set(credentials: OpenVpnCredentials): Promise<void>;
10
+ delete(): Promise<void>;
11
+ status(): Promise<CredentialStoreStatus>;
12
+ }
13
+ export declare class KeyringCredentialStore implements CredentialStore {
14
+ get(): Promise<OpenVpnCredentials | null>;
15
+ set(credentials: OpenVpnCredentials): Promise<void>;
16
+ delete(): Promise<void>;
17
+ status(): Promise<CredentialStoreStatus>;
18
+ private createEntry;
19
+ }
20
+ export declare class MemoryCredentialStore implements CredentialStore {
21
+ private credentials;
22
+ get(): Promise<OpenVpnCredentials | null>;
23
+ set(credentials: OpenVpnCredentials): Promise<void>;
24
+ delete(): Promise<void>;
25
+ status(): Promise<CredentialStoreStatus>;
26
+ }
@@ -0,0 +1,69 @@
1
+ import { SurfCliError } from '../core/errors.js';
2
+ import { openVpnCredentialsSchema } from '../core/types.js';
3
+ const SERVICE_NAME = 'surf-cli';
4
+ const ACCOUNT_NAME = 'openvpn-service-credentials';
5
+ export class KeyringCredentialStore {
6
+ async get() {
7
+ const entry = await this.createEntry();
8
+ const value = entry.getPassword();
9
+ if (!value) {
10
+ return null;
11
+ }
12
+ return openVpnCredentialsSchema.parse(JSON.parse(value));
13
+ }
14
+ async set(credentials) {
15
+ const parsed = openVpnCredentialsSchema.parse(credentials);
16
+ const entry = await this.createEntry();
17
+ entry.setPassword(JSON.stringify(parsed));
18
+ }
19
+ async delete() {
20
+ const entry = await this.createEntry();
21
+ try {
22
+ entry.deletePassword();
23
+ }
24
+ catch (error) {
25
+ if (error instanceof Error && /not found|no matching/i.test(error.message)) {
26
+ return;
27
+ }
28
+ throw error;
29
+ }
30
+ }
31
+ async status() {
32
+ try {
33
+ await this.createEntry();
34
+ return { available: true, backend: '@napi-rs/keyring' };
35
+ }
36
+ catch (error) {
37
+ return {
38
+ available: false,
39
+ backend: '@napi-rs/keyring',
40
+ message: error instanceof Error ? error.message : String(error)
41
+ };
42
+ }
43
+ }
44
+ async createEntry() {
45
+ try {
46
+ const keyring = (await import('@napi-rs/keyring'));
47
+ return new keyring.Entry(SERVICE_NAME, ACCOUNT_NAME);
48
+ }
49
+ catch (error) {
50
+ throw new SurfCliError('AUTH_REQUIRED', 'OS keyring is unavailable. Install a desktop keyring service, or run connect interactively to enter credentials for this session.', { cause: error });
51
+ }
52
+ }
53
+ }
54
+ export class MemoryCredentialStore {
55
+ credentials = null;
56
+ async get() {
57
+ return this.credentials;
58
+ }
59
+ async set(credentials) {
60
+ this.credentials = openVpnCredentialsSchema.parse(credentials);
61
+ }
62
+ async delete() {
63
+ this.credentials = null;
64
+ }
65
+ async status() {
66
+ return { available: true, backend: 'memory' };
67
+ }
68
+ }
69
+ //# sourceMappingURL=credential-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-store.js","sourceRoot":"","sources":["../../src/adapters/credential-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,wBAAwB,EAA2B,MAAM,kBAAkB,CAAC;AAyBrF,MAAM,YAAY,GAAG,UAAU,CAAC;AAChC,MAAM,YAAY,GAAG,6BAA6B,CAAC;AAEnD,MAAM,OAAO,sBAAsB;IACjC,KAAK,CAAC,GAAG;QACP,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACvC,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;QAClC,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,wBAAwB,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,WAA+B;QACvC,MAAM,MAAM,GAAG,wBAAwB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QAC3D,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACvC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,MAAM;QACV,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACvC,IAAI,CAAC;YACH,KAAK,CAAC,cAAc,EAAE,CAAC;QACzB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,KAAK,IAAI,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC3E,OAAO;YACT,CAAC;YAED,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;YACzB,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,kBAAkB,EAAE,CAAC;QAC1D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO;gBACL,SAAS,EAAE,KAAK;gBAChB,OAAO,EAAE,kBAAkB;gBAC3B,OAAO,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;aAChE,CAAC;QACJ,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,WAAW;QACvB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAkB,CAAC;YACpE,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC;QACvD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,IAAI,YAAY,CACpB,eAAe,EACf,mIAAmI,EACnI,EAAE,KAAK,EAAE,KAAK,EAAE,CACjB,CAAC;QACJ,CAAC;IACH,CAAC;CACF;AAED,MAAM,OAAO,qBAAqB;IACxB,WAAW,GAA8B,IAAI,CAAC;IAEtD,KAAK,CAAC,GAAG;QACP,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,WAA+B;QACvC,IAAI,CAAC,WAAW,GAAG,wBAAwB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;IACjE,CAAC;IAED,KAAK,CAAC,MAAM;QACV,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED,KAAK,CAAC,MAAM;QACV,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;IAChD,CAAC;CACF"}
@@ -0,0 +1,56 @@
1
+ import type { Protocol } from '../core/types.js';
2
+ export interface OpenVpnProcessInfo {
3
+ pid: number;
4
+ commandLine: string;
5
+ }
6
+ export interface OpenVpnStartOptions {
7
+ configPath: string;
8
+ authFilePath: string;
9
+ pidFilePath: string;
10
+ statusFilePath: string;
11
+ logFilePath: string;
12
+ foreground: boolean;
13
+ useSudo?: boolean;
14
+ initTimeoutMs?: number;
15
+ }
16
+ export interface OpenVpnStartResult {
17
+ pid: number;
18
+ }
19
+ export interface ParsedOpenVpnStatus {
20
+ updatedAt?: string;
21
+ statistics: Record<string, string>;
22
+ raw: string;
23
+ }
24
+ export declare class OpenVpnRunner {
25
+ start(options: OpenVpnStartOptions): Promise<OpenVpnStartResult>;
26
+ terminate(pid: number, useSudo?: boolean): Promise<void>;
27
+ isAlive(pid: number): Promise<boolean>;
28
+ isManagedOpenVpnProcess(pid: number, configPath: string): Promise<boolean>;
29
+ readStatus(statusFilePath: string): Promise<ParsedOpenVpnStatus | null>;
30
+ }
31
+ export declare function shouldIgnoreStatusReadError(error: unknown): boolean;
32
+ export declare function buildOpenVpnArgs(options: Pick<OpenVpnStartOptions, 'configPath' | 'authFilePath' | 'pidFilePath' | 'statusFilePath' | 'logFilePath'>): string[];
33
+ export declare function parseOpenVpnStatus(raw: string): ParsedOpenVpnStatus;
34
+ export declare function protocolToOpenVpnFileSuffix(protocol: Protocol): string;
35
+ export declare function findOpenVpnProcesses(): Promise<OpenVpnProcessInfo[]>;
36
+ export declare function terminateProcess(pid: number, signal?: NodeJS.Signals): Promise<boolean>;
37
+ interface ProcessOutput {
38
+ stdout: string;
39
+ stderr: string;
40
+ }
41
+ interface ChildExit {
42
+ code: number | null;
43
+ signal: NodeJS.Signals | null;
44
+ error?: string;
45
+ }
46
+ export declare function formatInitializationFailure(options: {
47
+ lastLog?: string;
48
+ processOutput?: ProcessOutput | undefined;
49
+ childExit?: ChildExit | null;
50
+ }): string;
51
+ export declare function formatDiagnosticOutput(options: {
52
+ lastLog?: string;
53
+ processOutput?: ProcessOutput | undefined;
54
+ childExit?: ChildExit | null;
55
+ }): string;
56
+ export {};