@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.js","sourceRoot":"","sources":["../../src/commands/prompt.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,IAAI,cAAc,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QAClD,MAAM,IAAI,YAAY,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IACnD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,0BAA0B;IAC9C,iBAAiB,CAAC,2DAA2D,CAAC,CAAC;IAC/E,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,EAAE,OAAO,EAAE,oCAAoC,EAAE,CAAC,CAAC;IAChF,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC;QACpC,OAAO,EAAE,oCAAoC;QAC7C,IAAI,EAAE,GAAG;KACV,CAAC,CAAC;IACH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AAChC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,QAAyB;IAC7D,iBAAiB,CACf,mFAAmF,CACpF,CAAC;IACF,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QAC5C,IAAI,EAAE,GAAG,OAAO,CAAC,YAAY,KAAK,OAAO,CAAC,QAAQ,CAAC,WAAW,EAAE,GAAG;QACnE,KAAK,EAAE,OAAO;KACf,CAAC,CAAC,CAAC;IAEJ,OAAO,MAAM,CAAC;QACZ,OAAO,EAAE,yBAAyB;QAClC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE;YACf,IAAI,CAAC,IAAI;gBAAE,OAAO,UAAU,CAAC;YAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;YACjC,OAAO,UAAU,CAAC,MAAM,CACtB,CAAC,MAAM,EAAE,EAAE,CACT,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC;gBACzC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAC7C,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,CAClD,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import { ServerCatalogService } from '../services/server-catalog-service.js';
3
+ export declare function createServersCommand(catalogService?: ServerCatalogService): Command;
@@ -0,0 +1,38 @@
1
+ import { Command, Option } from 'commander';
2
+ import { protocolSchema } from '../core/types.js';
3
+ import { ServerCatalogService } from '../services/server-catalog-service.js';
4
+ export function createServersCommand(catalogService = new ServerCatalogService()) {
5
+ const servers = new Command('servers').description('Manage cached Surfshark OpenVPN server profiles');
6
+ servers
7
+ .command('refresh')
8
+ .description('Download Surfshark OpenVPN configuration files')
9
+ .action(async () => {
10
+ const result = await catalogService.refresh();
11
+ console.log(`Downloaded ${result.written} OpenVPN profiles to ${result.directory}.`);
12
+ if (result.removed > 0) {
13
+ console.log(`Removed ${result.removed} stale profiles.`);
14
+ }
15
+ });
16
+ servers
17
+ .command('list')
18
+ .description('List cached Surfshark OpenVPN server profiles')
19
+ .addOption(new Option('--protocol <protocol>', 'filter protocol').choices(['udp', 'tcp']))
20
+ .option('--json', 'print machine-readable JSON')
21
+ .action(async (options) => {
22
+ const protocol = options.protocol ? protocolSchema.parse(options.protocol) : undefined;
23
+ const profiles = await catalogService.list(protocol);
24
+ if (options.json) {
25
+ console.log(JSON.stringify(profiles, null, 2));
26
+ return;
27
+ }
28
+ if (profiles.length === 0) {
29
+ console.log('No cached profiles. Run `surf-cli servers refresh`.');
30
+ return;
31
+ }
32
+ for (const profile of profiles) {
33
+ console.log(`${profile.id.padEnd(24)} ${profile.host.padEnd(36)} ${profile.protocol.toUpperCase()}`);
34
+ }
35
+ });
36
+ return servers;
37
+ }
38
+ //# sourceMappingURL=servers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"servers.js","sourceRoot":"","sources":["../../src/commands/servers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAiB,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAE7E,MAAM,UAAU,oBAAoB,CAAC,cAAc,GAAG,IAAI,oBAAoB,EAAE;IAC9E,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,SAAS,CAAC,CAAC,WAAW,CAChD,iDAAiD,CAClD,CAAC;IAEF,OAAO;SACJ,OAAO,CAAC,SAAS,CAAC;SAClB,WAAW,CAAC,gDAAgD,CAAC;SAC7D,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,OAAO,EAAE,CAAC;QAC9C,OAAO,CAAC,GAAG,CAAC,cAAc,MAAM,CAAC,OAAO,wBAAwB,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC;QACrF,IAAI,MAAM,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,OAAO,kBAAkB,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,OAAO;SACJ,OAAO,CAAC,MAAM,CAAC;SACf,WAAW,CAAC,+CAA+C,CAAC;SAC5D,SAAS,CAAC,IAAI,MAAM,CAAC,uBAAuB,EAAE,iBAAiB,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;SACzF,MAAM,CAAC,QAAQ,EAAE,6BAA6B,CAAC;SAC/C,MAAM,CAAC,KAAK,EAAE,OAAgD,EAAE,EAAE;QACjE,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACvF,MAAM,QAAQ,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrD,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAC/C,OAAO;QACT,CAAC;QAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,GAAG,CAAC,qDAAqD,CAAC,CAAC;YACnE,OAAO;QACT,CAAC;QAED,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;YAC/B,OAAO,CAAC,GAAG,CACT,GAAG,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,WAAW,EAAE,EAAE,CACxF,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { Command } from 'commander';
2
+ import { ServerCatalogService } from '../services/server-catalog-service.js';
3
+ import { VpnService } from '../services/vpn-service.js';
4
+ export declare function createConnectCommand(vpnService?: VpnService, catalogService?: ServerCatalogService): Command;
5
+ export declare function createDisconnectCommand(vpnService?: VpnService): Command;
6
+ export declare function createStatusCommand(vpnService?: VpnService): Command;
7
+ export declare function createKillCommand(): Command;
@@ -0,0 +1,137 @@
1
+ import { setTimeout as delay } from 'node:timers/promises';
2
+ import { Command, Option } from 'commander';
3
+ import { confirm } from '@inquirer/prompts';
4
+ import { protocolSchema } from '../core/types.js';
5
+ import { SurfCliError } from '../core/errors.js';
6
+ import { ServerCatalogService } from '../services/server-catalog-service.js';
7
+ import { VpnService } from '../services/vpn-service.js';
8
+ import { findOpenVpnProcesses, terminateProcess } from '../adapters/openvpn-runner.js';
9
+ import { promptForServer, assertInteractive } from './prompt.js';
10
+ export function createConnectCommand(vpnService = new VpnService(), catalogService = new ServerCatalogService()) {
11
+ return new Command('connect')
12
+ .description('Connect to a Surfshark OpenVPN server')
13
+ .argument('[server]', 'server id, location code, host, or unique substring')
14
+ .addOption(new Option('--protocol <protocol>', 'OpenVPN transport protocol')
15
+ .choices(['udp', 'tcp'])
16
+ .default('udp'))
17
+ .option('--foreground', 'run OpenVPN in the foreground')
18
+ .action(async (server, options) => {
19
+ const protocol = protocolSchema.parse(options.protocol);
20
+ let serverQuery = server;
21
+ if (!serverQuery) {
22
+ let profiles = await catalogService.list(protocol);
23
+ if (profiles.length === 0) {
24
+ console.error('No cached Surfshark profiles found. Downloading profiles first...');
25
+ await catalogService.refresh();
26
+ profiles = await catalogService.list(protocol);
27
+ }
28
+ if (profiles.length === 0) {
29
+ throw new SurfCliError('CONFIG_ERROR', 'No Surfshark OpenVPN profiles are available.');
30
+ }
31
+ serverQuery = (await promptForServer(profiles)).locationCode;
32
+ }
33
+ const activeConnection = await vpnService.connect(serverQuery, protocol, {
34
+ ...(options.foreground === undefined ? {} : { foreground: options.foreground })
35
+ });
36
+ if (!options.foreground) {
37
+ console.log(`Connected to ${activeConnection.serverId} with OpenVPN PID ${activeConnection.pid}. Run \`surf-cli status\` for details.`);
38
+ }
39
+ });
40
+ }
41
+ export function createDisconnectCommand(vpnService = new VpnService()) {
42
+ return new Command('disconnect')
43
+ .description('Disconnect the surf-cli managed OpenVPN session')
44
+ .action(async () => {
45
+ const stopped = await vpnService.disconnect();
46
+ if (!stopped) {
47
+ console.log('No surf-cli managed OpenVPN connection was active.');
48
+ return;
49
+ }
50
+ console.log(`Disconnected ${stopped.serverId} (PID ${stopped.pid}).`);
51
+ });
52
+ }
53
+ export function createStatusCommand(vpnService = new VpnService()) {
54
+ return new Command('status')
55
+ .description('Show surf-cli managed OpenVPN connection status')
56
+ .option('--json', 'print machine-readable JSON')
57
+ .action(async (options) => {
58
+ const status = await vpnService.status();
59
+ if (options.json) {
60
+ console.log(JSON.stringify(status, null, 2));
61
+ return;
62
+ }
63
+ if (!status.connected) {
64
+ console.log('Disconnected');
65
+ return;
66
+ }
67
+ const active = status.activeConnection;
68
+ console.log(`Connected: ${active.serverId}`);
69
+ console.log(`PID: ${active.pid}`);
70
+ console.log(`Started: ${active.startedAt}`);
71
+ if (status.openVpnStatus?.updatedAt) {
72
+ console.log(`OpenVPN status updated: ${status.openVpnStatus.updatedAt}`);
73
+ }
74
+ });
75
+ }
76
+ export function createKillCommand() {
77
+ return new Command('kill')
78
+ .description('Kill all running OpenVPN processes (last resort)')
79
+ .option('--force', 'skip confirmation prompt')
80
+ .action(async (options) => {
81
+ const processes = await findOpenVpnProcesses();
82
+ if (processes.length === 0) {
83
+ console.log('No running OpenVPN processes found.');
84
+ return;
85
+ }
86
+ console.log(`Found ${processes.length} OpenVPN process(es):`);
87
+ for (const proc of processes) {
88
+ console.log(` PID ${proc.pid}: ${proc.commandLine}`);
89
+ }
90
+ if (!options.force) {
91
+ assertInteractive('Kill confirmation requires an interactive terminal. Use --force to skip.');
92
+ const answer = await confirm({
93
+ message: `Kill ${processes.length} OpenVPN process(es)?`,
94
+ default: false
95
+ });
96
+ if (!answer) {
97
+ console.log('Aborted.');
98
+ return;
99
+ }
100
+ }
101
+ let killed = 0;
102
+ let forceKilled = 0;
103
+ for (const proc of processes) {
104
+ const terminated = await terminateProcess(proc.pid, 'SIGTERM');
105
+ if (!terminated) {
106
+ console.error(`Failed to send SIGTERM to PID ${proc.pid}.`);
107
+ continue;
108
+ }
109
+ killed++;
110
+ }
111
+ // Wait briefly then check for survivors
112
+ await delay(1000);
113
+ for (const proc of processes) {
114
+ try {
115
+ process.kill(proc.pid, 0);
116
+ // Still alive — escalate to SIGKILL
117
+ const forceTerminated = await terminateProcess(proc.pid, 'SIGKILL');
118
+ if (forceTerminated) {
119
+ forceKilled++;
120
+ }
121
+ }
122
+ catch {
123
+ // Process exited (ESRCH) — good
124
+ }
125
+ }
126
+ const sigtermed = killed - forceKilled;
127
+ const parts = [];
128
+ if (sigtermed > 0) {
129
+ parts.push(`${sigtermed} terminated (SIGTERM)`);
130
+ }
131
+ if (forceKilled > 0) {
132
+ parts.push(`${forceKilled} force-killed (SIGKILL)`);
133
+ }
134
+ console.log(`Done. ${parts.join(', ')}.`);
135
+ });
136
+ }
137
+ //# sourceMappingURL=vpn.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vpn.js","sourceRoot":"","sources":["../../src/commands/vpn.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAiB,MAAM,kBAAkB,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACxD,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACvF,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEjE,MAAM,UAAU,oBAAoB,CAClC,UAAU,GAAG,IAAI,UAAU,EAAE,EAC7B,cAAc,GAAG,IAAI,oBAAoB,EAAE;IAE3C,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC;SAC1B,WAAW,CAAC,uCAAuC,CAAC;SACpD,QAAQ,CAAC,UAAU,EAAE,qDAAqD,CAAC;SAC3E,SAAS,CACR,IAAI,MAAM,CAAC,uBAAuB,EAAE,4BAA4B,CAAC;SAC9D,OAAO,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;SACvB,OAAO,CAAC,KAAK,CAAC,CAClB;SACA,MAAM,CAAC,cAAc,EAAE,+BAA+B,CAAC;SACvD,MAAM,CACL,KAAK,EAAE,MAA0B,EAAE,OAAqD,EAAE,EAAE;QAC1F,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACxD,IAAI,WAAW,GAAG,MAAM,CAAC;QAEzB,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,IAAI,QAAQ,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACnD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1B,OAAO,CAAC,KAAK,CAAC,mEAAmE,CAAC,CAAC;gBACnF,MAAM,cAAc,CAAC,OAAO,EAAE,CAAC;gBAC/B,QAAQ,GAAG,MAAM,cAAc,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACjD,CAAC;YAED,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBAC1B,MAAM,IAAI,YAAY,CAAC,cAAc,EAAE,8CAA8C,CAAC,CAAC;YACzF,CAAC;YAED,WAAW,GAAG,CAAC,MAAM,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC;QAC/D,CAAC;QAED,MAAM,gBAAgB,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,WAAW,EAAE,QAAQ,EAAE;YACvE,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;SAChF,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;YACxB,OAAO,CAAC,GAAG,CACT,gBAAgB,gBAAgB,CAAC,QAAQ,qBAAqB,gBAAgB,CAAC,GAAG,wCAAwC,CAC3H,CAAC;QACJ,CAAC;IACH,CAAC,CACF,CAAC;AACN,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,UAAU,GAAG,IAAI,UAAU,EAAE;IACnE,OAAO,IAAI,OAAO,CAAC,YAAY,CAAC;SAC7B,WAAW,CAAC,iDAAiD,CAAC;SAC9D,MAAM,CAAC,KAAK,IAAI,EAAE;QACjB,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,UAAU,EAAE,CAAC;QAC9C,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,CAAC,GAAG,CAAC,oDAAoD,CAAC,CAAC;YAClE,OAAO;QACT,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,gBAAgB,OAAO,CAAC,QAAQ,SAAS,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;IACxE,CAAC,CAAC,CAAC;AACP,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,UAAU,GAAG,IAAI,UAAU,EAAE;IAC/D,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC;SACzB,WAAW,CAAC,iDAAiD,CAAC;SAC9D,MAAM,CAAC,QAAQ,EAAE,6BAA6B,CAAC;SAC/C,MAAM,CAAC,KAAK,EAAE,OAA2B,EAAE,EAAE;QAC5C,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC;QACzC,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;YAC7C,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,CAAC,gBAAiB,CAAC;QACxC,OAAO,CAAC,GAAG,CAAC,cAAc,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC7C,OAAO,CAAC,GAAG,CAAC,QAAQ,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC;QAClC,OAAO,CAAC,GAAG,CAAC,YAAY,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;QAC5C,IAAI,MAAM,CAAC,aAAa,EAAE,SAAS,EAAE,CAAC;YACpC,OAAO,CAAC,GAAG,CAAC,2BAA2B,MAAM,CAAC,aAAa,CAAC,SAAS,EAAE,CAAC,CAAC;QAC3E,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;SACvB,WAAW,CAAC,kDAAkD,CAAC;SAC/D,MAAM,CAAC,SAAS,EAAE,0BAA0B,CAAC;SAC7C,MAAM,CAAC,KAAK,EAAE,OAA4B,EAAE,EAAE;QAC7C,MAAM,SAAS,GAAG,MAAM,oBAAoB,EAAE,CAAC;QAE/C,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,CAAC,GAAG,CAAC,qCAAqC,CAAC,CAAC;YACnD,OAAO;QACT,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,SAAS,SAAS,CAAC,MAAM,uBAAuB,CAAC,CAAC;QAC9D,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,OAAO,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,GAAG,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QACxD,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACnB,iBAAiB,CACf,0EAA0E,CAC3E,CAAC;YACF,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;gBAC3B,OAAO,EAAE,QAAQ,SAAS,CAAC,MAAM,uBAAuB;gBACxD,OAAO,EAAE,KAAK;aACf,CAAC,CAAC;YAEH,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;gBACxB,OAAO;YACT,CAAC;QACH,CAAC;QAED,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,IAAI,WAAW,GAAG,CAAC,CAAC;QAEpB,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,MAAM,UAAU,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;YAC/D,IAAI,CAAC,UAAU,EAAE,CAAC;gBAChB,OAAO,CAAC,KAAK,CAAC,iCAAiC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;gBAC5D,SAAS;YACX,CAAC;YACD,MAAM,EAAE,CAAC;QACX,CAAC;QAED,wCAAwC;QACxC,MAAM,KAAK,CAAC,IAAI,CAAC,CAAC;QAElB,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC;gBACH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBAC1B,oCAAoC;gBACpC,MAAM,eAAe,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;gBACpE,IAAI,eAAe,EAAE,CAAC;oBACpB,WAAW,EAAE,CAAC;gBAChB,CAAC;YACH,CAAC;YAAC,MAAM,CAAC;gBACP,gCAAgC;YAClC,CAAC;QACH,CAAC;QAED,MAAM,SAAS,GAAG,MAAM,GAAG,WAAW,CAAC;QACvC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;YAClB,KAAK,CAAC,IAAI,CAAC,GAAG,SAAS,uBAAuB,CAAC,CAAC;QAClD,CAAC;QACD,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YACpB,KAAK,CAAC,IAAI,CAAC,GAAG,WAAW,yBAAyB,CAAC,CAAC;QACtD,CAAC;QACD,OAAO,CAAC,GAAG,CAAC,SAAS,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACP,CAAC"}
@@ -0,0 +1,12 @@
1
+ export type SurfCliErrorCode = 'INVALID_USAGE' | 'DEPENDENCY_MISSING' | 'AUTH_REQUIRED' | 'AUTH_FAILED' | 'CONNECTION_FAILED' | 'NETWORK_ERROR' | 'CONFIG_ERROR' | 'UNSUPPORTED_PLATFORM';
2
+ export type SurfCliExitCode = 1 | 2 | 3 | 4 | 5;
3
+ export declare class SurfCliError extends Error {
4
+ readonly code: SurfCliErrorCode;
5
+ readonly exitCode: SurfCliExitCode;
6
+ constructor(code: SurfCliErrorCode, message: string, options?: {
7
+ cause?: unknown;
8
+ });
9
+ }
10
+ export declare function isSurfCliError(error: unknown): error is SurfCliError;
11
+ export declare function redactSecrets(input: string): string;
12
+ export declare function toErrorMessage(error: unknown): string;
@@ -0,0 +1,37 @@
1
+ const EXIT_CODE_BY_ERROR_CODE = {
2
+ INVALID_USAGE: 2,
3
+ DEPENDENCY_MISSING: 3,
4
+ AUTH_REQUIRED: 4,
5
+ AUTH_FAILED: 4,
6
+ CONNECTION_FAILED: 5,
7
+ NETWORK_ERROR: 1,
8
+ CONFIG_ERROR: 1,
9
+ UNSUPPORTED_PLATFORM: 3
10
+ };
11
+ export class SurfCliError extends Error {
12
+ code;
13
+ exitCode;
14
+ constructor(code, message, options) {
15
+ super(message, options);
16
+ this.name = 'SurfCliError';
17
+ this.code = code;
18
+ this.exitCode = EXIT_CODE_BY_ERROR_CODE[code];
19
+ }
20
+ }
21
+ export function isSurfCliError(error) {
22
+ return error instanceof SurfCliError;
23
+ }
24
+ export function redactSecrets(input) {
25
+ return input
26
+ .replace(/(password\s*[=:]\s*)[^\s,;]+/gi, '$1[REDACTED]')
27
+ .replace(/(token\s*[=:]\s*)[^\s,;]+/gi, '$1[REDACTED]')
28
+ .replace(/(authorization:\s*bearer\s+)[^\s,;]+/gi, '$1[REDACTED]')
29
+ .replace(/(auth-user-pass\s+)[^\s]+/gi, '$1[REDACTED]');
30
+ }
31
+ export function toErrorMessage(error) {
32
+ if (error instanceof Error) {
33
+ return redactSecrets(error.message);
34
+ }
35
+ return redactSecrets(String(error));
36
+ }
37
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAYA,MAAM,uBAAuB,GAA8C;IACzE,aAAa,EAAE,CAAC;IAChB,kBAAkB,EAAE,CAAC;IACrB,aAAa,EAAE,CAAC;IAChB,WAAW,EAAE,CAAC;IACd,iBAAiB,EAAE,CAAC;IACpB,aAAa,EAAE,CAAC;IAChB,YAAY,EAAE,CAAC;IACf,oBAAoB,EAAE,CAAC;CACxB,CAAC;AAEF,MAAM,OAAO,YAAa,SAAQ,KAAK;IAC5B,IAAI,CAAmB;IACvB,QAAQ,CAAkB;IAEnC,YAAY,IAAsB,EAAE,OAAe,EAAE,OAA6B;QAChF,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC3B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,QAAQ,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAC;IAChD,CAAC;CACF;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,KAAK,YAAY,YAAY,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,OAAO,KAAK;SACT,OAAO,CAAC,gCAAgC,EAAE,cAAc,CAAC;SACzD,OAAO,CAAC,6BAA6B,EAAE,cAAc,CAAC;SACtD,OAAO,CAAC,wCAAwC,EAAE,cAAc,CAAC;SACjE,OAAO,CAAC,6BAA6B,EAAE,cAAc,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;QAC3B,OAAO,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IACtC,CAAC;IAED,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;AACtC,CAAC"}
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod';
2
+ export declare const protocolSchema: z.ZodEnum<{
3
+ udp: "udp";
4
+ tcp: "tcp";
5
+ }>;
6
+ export type Protocol = z.infer<typeof protocolSchema>;
7
+ export declare const openVpnCredentialsSchema: z.ZodObject<{
8
+ username: z.ZodString;
9
+ password: z.ZodString;
10
+ }, z.core.$strip>;
11
+ export type OpenVpnCredentials = z.infer<typeof openVpnCredentialsSchema>;
12
+ export declare const serverProfileSchema: z.ZodObject<{
13
+ id: z.ZodString;
14
+ host: z.ZodString;
15
+ locationCode: z.ZodString;
16
+ protocol: z.ZodEnum<{
17
+ udp: "udp";
18
+ tcp: "tcp";
19
+ }>;
20
+ fileName: z.ZodString;
21
+ configPath: z.ZodString;
22
+ }, z.core.$strip>;
23
+ export type ServerProfile = z.infer<typeof serverProfileSchema>;
24
+ export declare const activeConnectionSchema: z.ZodObject<{
25
+ pid: z.ZodNumber;
26
+ serverId: z.ZodString;
27
+ protocol: z.ZodEnum<{
28
+ udp: "udp";
29
+ tcp: "tcp";
30
+ }>;
31
+ configPath: z.ZodString;
32
+ authFilePath: z.ZodString;
33
+ pidFilePath: z.ZodString;
34
+ statusFilePath: z.ZodString;
35
+ logFilePath: z.ZodString;
36
+ startedAt: z.ZodString;
37
+ }, z.core.$strip>;
38
+ export type ActiveConnection = z.infer<typeof activeConnectionSchema>;
39
+ export declare const appStateSchema: z.ZodObject<{
40
+ activeConnection: z.ZodOptional<z.ZodObject<{
41
+ pid: z.ZodNumber;
42
+ serverId: z.ZodString;
43
+ protocol: z.ZodEnum<{
44
+ udp: "udp";
45
+ tcp: "tcp";
46
+ }>;
47
+ configPath: z.ZodString;
48
+ authFilePath: z.ZodString;
49
+ pidFilePath: z.ZodString;
50
+ statusFilePath: z.ZodString;
51
+ logFilePath: z.ZodString;
52
+ startedAt: z.ZodString;
53
+ }, z.core.$strip>>;
54
+ }, z.core.$strip>;
55
+ export type AppState = z.infer<typeof appStateSchema>;
56
+ export type OutputMode = 'text' | 'json';
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+ export const protocolSchema = z.enum(['udp', 'tcp']);
3
+ export const openVpnCredentialsSchema = z.object({
4
+ username: z.string().min(1, 'username is required'),
5
+ password: z.string().min(1, 'password is required')
6
+ });
7
+ export const serverProfileSchema = z.object({
8
+ id: z.string().min(1),
9
+ host: z.string().min(1),
10
+ locationCode: z.string().min(1),
11
+ protocol: protocolSchema,
12
+ fileName: z.string().min(1),
13
+ configPath: z.string().min(1)
14
+ });
15
+ export const activeConnectionSchema = z.object({
16
+ pid: z.number().int().positive(),
17
+ serverId: z.string().min(1),
18
+ protocol: protocolSchema,
19
+ configPath: z.string().min(1),
20
+ authFilePath: z.string().min(1),
21
+ pidFilePath: z.string().min(1),
22
+ statusFilePath: z.string().min(1),
23
+ logFilePath: z.string().min(1),
24
+ startedAt: z.string().datetime()
25
+ });
26
+ export const appStateSchema = z.object({
27
+ activeConnection: activeConnectionSchema.optional()
28
+ });
29
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC;AAGrD,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,sBAAsB,CAAC;IACnD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,sBAAsB,CAAC;CACpD,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACvB,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,QAAQ,EAAE,cAAc;IACxB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;CAC9B,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAChC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3B,QAAQ,EAAE,cAAc;IACxB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/B,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC9B,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACjC,CAAC,CAAC;AAIH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,gBAAgB,EAAE,sBAAsB,CAAC,QAAQ,EAAE;CACpD,CAAC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { type OpenVpnCredentials } from '../core/types.js';
2
+ import { type CredentialStore, type CredentialStoreStatus } from '../adapters/credential-store.js';
3
+ import { SurfsharkAccountClient, type CodeLoginOutcome, type EmailLoginOutcome, type LoginCodeChallenge } from '../adapters/surfshark-account-client.js';
4
+ export declare class AuthService {
5
+ private readonly credentialStore;
6
+ private readonly accountClient;
7
+ constructor(credentialStore?: CredentialStore, accountClient?: SurfsharkAccountClient);
8
+ getCredentials(): Promise<OpenVpnCredentials | null>;
9
+ saveManualCredentials(credentials: OpenVpnCredentials): Promise<void>;
10
+ clearCredentials(): Promise<void>;
11
+ credentialStoreStatus(): Promise<CredentialStoreStatus>;
12
+ loginWithEmail(email: string, password: string): Promise<EmailLoginOutcome>;
13
+ createLoginCodeChallenge(): Promise<LoginCodeChallenge>;
14
+ waitForLoginCodeActivation(challenge: LoginCodeChallenge): Promise<CodeLoginOutcome>;
15
+ requireCredentials(): Promise<OpenVpnCredentials>;
16
+ }
@@ -0,0 +1,49 @@
1
+ import { SurfCliError } from '../core/errors.js';
2
+ import { openVpnCredentialsSchema } from '../core/types.js';
3
+ import { KeyringCredentialStore } from '../adapters/credential-store.js';
4
+ import { SurfsharkAccountClient } from '../adapters/surfshark-account-client.js';
5
+ export class AuthService {
6
+ credentialStore;
7
+ accountClient;
8
+ constructor(credentialStore = new KeyringCredentialStore(), accountClient = new SurfsharkAccountClient()) {
9
+ this.credentialStore = credentialStore;
10
+ this.accountClient = accountClient;
11
+ }
12
+ async getCredentials() {
13
+ return this.credentialStore.get();
14
+ }
15
+ async saveManualCredentials(credentials) {
16
+ await this.credentialStore.set(openVpnCredentialsSchema.parse(credentials));
17
+ }
18
+ async clearCredentials() {
19
+ await this.credentialStore.delete();
20
+ }
21
+ async credentialStoreStatus() {
22
+ return this.credentialStore.status();
23
+ }
24
+ async loginWithEmail(email, password) {
25
+ const outcome = await this.accountClient.loginWithEmail(email, password);
26
+ if (outcome.credentials) {
27
+ await this.saveManualCredentials(outcome.credentials);
28
+ }
29
+ return outcome;
30
+ }
31
+ async createLoginCodeChallenge() {
32
+ return this.accountClient.createLoginCodeChallenge();
33
+ }
34
+ async waitForLoginCodeActivation(challenge) {
35
+ const outcome = await this.accountClient.waitForLoginCodeActivation(challenge);
36
+ if (outcome.credentials) {
37
+ await this.saveManualCredentials(outcome.credentials);
38
+ }
39
+ return outcome;
40
+ }
41
+ async requireCredentials() {
42
+ const credentials = await this.getCredentials();
43
+ if (!credentials) {
44
+ throw new SurfCliError('AUTH_REQUIRED', 'OpenVPN service credentials are not configured. Run `surf-cli auth login --manual`.');
45
+ }
46
+ return credentials;
47
+ }
48
+ }
49
+ //# sourceMappingURL=auth-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth-service.js","sourceRoot":"","sources":["../../src/services/auth-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,wBAAwB,EAA2B,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,sBAAsB,EAGvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,sBAAsB,EAIvB,MAAM,yCAAyC,CAAC;AAEjD,MAAM,OAAO,WAAW;IAEH;IACA;IAFnB,YACmB,kBAAmC,IAAI,sBAAsB,EAAE,EAC/D,gBAAgB,IAAI,sBAAsB,EAAE;QAD5C,oBAAe,GAAf,eAAe,CAAgD;QAC/D,kBAAa,GAAb,aAAa,CAA+B;IAC5D,CAAC;IAEJ,KAAK,CAAC,cAAc;QAClB,OAAO,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,qBAAqB,CAAC,WAA+B;QACzD,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,wBAAwB,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC;IAC9E,CAAC;IAED,KAAK,CAAC,gBAAgB;QACpB,MAAM,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,qBAAqB;QACzB,OAAO,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,CAAC;IACvC,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,KAAa,EAAE,QAAgB;QAClD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;QACzE,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACxB,MAAM,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACxD,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,wBAAwB;QAC5B,OAAO,IAAI,CAAC,aAAa,CAAC,wBAAwB,EAAE,CAAC;IACvD,CAAC;IAED,KAAK,CAAC,0BAA0B,CAAC,SAA6B;QAC5D,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,0BAA0B,CAAC,SAAS,CAAC,CAAC;QAC/E,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;YACxB,MAAM,IAAI,CAAC,qBAAqB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACxD,CAAC;QAED,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,cAAc,EAAE,CAAC;QAChD,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,MAAM,IAAI,YAAY,CACpB,eAAe,EACf,qFAAqF,CACtF,CAAC;QACJ,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;CACF"}
@@ -0,0 +1,11 @@
1
+ import { type AppPaths } from '../adapters/xdg-paths.js';
2
+ export interface DoctorCheck {
3
+ name: string;
4
+ ok: boolean;
5
+ message: string;
6
+ }
7
+ export declare class DoctorService {
8
+ private readonly paths;
9
+ constructor(paths?: AppPaths);
10
+ run(): Promise<DoctorCheck[]>;
11
+ }
@@ -0,0 +1,61 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { access } from 'node:fs/promises';
3
+ import { constants } from 'node:fs';
4
+ import { getAppPaths } from '../adapters/xdg-paths.js';
5
+ export class DoctorService {
6
+ paths;
7
+ constructor(paths = getAppPaths()) {
8
+ this.paths = paths;
9
+ }
10
+ async run() {
11
+ const checks = [];
12
+ checks.push(checkPlatform());
13
+ checks.push(await checkCommand('openvpn'));
14
+ checks.push(await checkCommand('sudo'));
15
+ checks.push(await checkOpenVpnProfiles(this.paths.openVpnConfigDir));
16
+ return checks;
17
+ }
18
+ }
19
+ function checkPlatform() {
20
+ const supported = process.platform === 'linux' || process.platform === 'darwin';
21
+ return {
22
+ name: 'platform',
23
+ ok: supported,
24
+ message: supported
25
+ ? `${process.platform} is supported${process.platform === 'darwin' ? ' on a best-effort basis' : ''}.`
26
+ : `${process.platform} is not supported. Linux is the primary target; macOS is best-effort.`
27
+ };
28
+ }
29
+ async function checkCommand(command) {
30
+ const exists = await commandExists(command);
31
+ return {
32
+ name: command,
33
+ ok: exists,
34
+ message: exists ? `${command} is available in PATH.` : `${command} was not found in PATH.`
35
+ };
36
+ }
37
+ async function checkOpenVpnProfiles(directory) {
38
+ try {
39
+ await access(directory, constants.R_OK);
40
+ return {
41
+ name: 'profiles',
42
+ ok: true,
43
+ message: `OpenVPN profile cache exists at ${directory}.`
44
+ };
45
+ }
46
+ catch {
47
+ return {
48
+ name: 'profiles',
49
+ ok: false,
50
+ message: 'OpenVPN profiles are not cached yet. Run `surf-cli servers refresh`.'
51
+ };
52
+ }
53
+ }
54
+ async function commandExists(command) {
55
+ return new Promise((resolve) => {
56
+ const child = spawn('sh', ['-c', `command -v ${command}`], { stdio: 'ignore' });
57
+ child.on('close', (code) => resolve(code === 0));
58
+ child.on('error', () => resolve(false));
59
+ });
60
+ }
61
+ //# sourceMappingURL=doctor-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor-service.js","sourceRoot":"","sources":["../../src/services/doctor-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACpC,OAAO,EAAE,WAAW,EAAiB,MAAM,0BAA0B,CAAC;AAQtE,MAAM,OAAO,aAAa;IACK;IAA7B,YAA6B,QAAkB,WAAW,EAAE;QAA/B,UAAK,GAAL,KAAK,CAA0B;IAAG,CAAC;IAEhE,KAAK,CAAC,GAAG;QACP,MAAM,MAAM,GAAkB,EAAE,CAAC;QACjC,MAAM,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC,CAAC;QAC7B,MAAM,CAAC,IAAI,CAAC,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;QAC3C,MAAM,CAAC,IAAI,CAAC,MAAM,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;QACxC,MAAM,CAAC,IAAI,CAAC,MAAM,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,CAAC;QACrE,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAED,SAAS,aAAa;IACpB,MAAM,SAAS,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC;IAChF,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,EAAE,EAAE,SAAS;QACb,OAAO,EAAE,SAAS;YAChB,CAAC,CAAC,GAAG,OAAO,CAAC,QAAQ,gBAAgB,OAAO,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,EAAE,GAAG;YACtG,CAAC,CAAC,GAAG,OAAO,CAAC,QAAQ,uEAAuE;KAC/F,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,YAAY,CAAC,OAAe;IACzC,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO;QACL,IAAI,EAAE,OAAO;QACb,EAAE,EAAE,MAAM;QACV,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,OAAO,wBAAwB,CAAC,CAAC,CAAC,GAAG,OAAO,yBAAyB;KAC3F,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,oBAAoB,CAAC,SAAiB;IACnD,IAAI,CAAC;QACH,MAAM,MAAM,CAAC,SAAS,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC;QACxC,OAAO;YACL,IAAI,EAAE,UAAU;YAChB,EAAE,EAAE,IAAI;YACR,OAAO,EAAE,mCAAmC,SAAS,GAAG;SACzD,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;YACL,IAAI,EAAE,UAAU;YAChB,EAAE,EAAE,KAAK;YACT,OAAO,EAAE,sEAAsE;SAChF,CAAC;IACJ,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,OAAe;IAC1C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,cAAc,OAAO,EAAE,CAAC,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAChF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC;QACjD,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { type Protocol, type ServerProfile } from '../core/types.js';
2
+ import { SurfsharkConfigClient } from '../adapters/surfshark-config-client.js';
3
+ import { type AppPaths } from '../adapters/xdg-paths.js';
4
+ export interface RefreshResult {
5
+ written: number;
6
+ removed: number;
7
+ directory: string;
8
+ }
9
+ export declare class ServerCatalogService {
10
+ private readonly configClient;
11
+ private readonly paths;
12
+ constructor(configClient?: SurfsharkConfigClient, paths?: AppPaths);
13
+ refresh(): Promise<RefreshResult>;
14
+ list(protocol?: Protocol): Promise<ServerProfile[]>;
15
+ resolve(query: string, protocol: Protocol): Promise<ServerProfile>;
16
+ }
17
+ export declare function parseProfileFile(fileName: string, directory: string): ServerProfile | null;
@@ -0,0 +1,112 @@
1
+ import { readdir, rm, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { unzipSync } from 'fflate';
4
+ import { SurfCliError } from '../core/errors.js';
5
+ import { protocolSchema } from '../core/types.js';
6
+ import { SurfsharkConfigClient } from '../adapters/surfshark-config-client.js';
7
+ import { ensureAppDirectories, getAppPaths } from '../adapters/xdg-paths.js';
8
+ const PROFILE_FILE_PATTERN = /^(?<locationCode>[a-z]{2}-[a-z0-9-]+)\.prod\.surfshark\.com_(?<protocol>udp|tcp)\.ovpn$/;
9
+ export class ServerCatalogService {
10
+ configClient;
11
+ paths;
12
+ constructor(configClient = new SurfsharkConfigClient(), paths = getAppPaths()) {
13
+ this.configClient = configClient;
14
+ this.paths = paths;
15
+ }
16
+ async refresh() {
17
+ await ensureAppDirectories(this.paths);
18
+ const archive = await this.configClient.fetchOpenVpnConfigurations();
19
+ const entries = unzipSync(archive);
20
+ const writtenFiles = new Set();
21
+ for (const [entryName, data] of Object.entries(entries)) {
22
+ const safeName = assertSafeProfileName(entryName);
23
+ const targetPath = path.join(this.paths.openVpnConfigDir, safeName);
24
+ await writeFile(targetPath, data, { mode: 0o600 });
25
+ writtenFiles.add(safeName);
26
+ }
27
+ let removed = 0;
28
+ for (const existing of await safeReadDir(this.paths.openVpnConfigDir)) {
29
+ if (existing.endsWith('.ovpn') && !writtenFiles.has(existing)) {
30
+ await rm(path.join(this.paths.openVpnConfigDir, existing), { force: true });
31
+ removed += 1;
32
+ }
33
+ }
34
+ return {
35
+ written: writtenFiles.size,
36
+ removed,
37
+ directory: this.paths.openVpnConfigDir
38
+ };
39
+ }
40
+ async list(protocol) {
41
+ await ensureAppDirectories(this.paths);
42
+ const profiles = (await safeReadDir(this.paths.openVpnConfigDir))
43
+ .map((fileName) => parseProfileFile(fileName, this.paths.openVpnConfigDir))
44
+ .filter((profile) => profile !== null)
45
+ .filter((profile) => (protocol ? profile.protocol === protocol : true))
46
+ .sort((a, b) => a.id.localeCompare(b.id));
47
+ return profiles;
48
+ }
49
+ async resolve(query, protocol) {
50
+ const normalizedQuery = query.trim().toLowerCase();
51
+ if (!normalizedQuery) {
52
+ throw new SurfCliError('INVALID_USAGE', 'Server name is required. Run `surf-cli servers list`.');
53
+ }
54
+ const profiles = await this.list(protocol);
55
+ const candidates = profiles.filter((profile) => {
56
+ return (profile.fileName.toLowerCase() === normalizedQuery ||
57
+ profile.id.toLowerCase() === normalizedQuery ||
58
+ profile.host.toLowerCase() === normalizedQuery ||
59
+ profile.locationCode.toLowerCase() === normalizedQuery ||
60
+ profile.id.toLowerCase().includes(normalizedQuery));
61
+ });
62
+ if (candidates.length === 1) {
63
+ return candidates[0];
64
+ }
65
+ if (candidates.length > 1) {
66
+ throw new SurfCliError('INVALID_USAGE', `Server query "${query}" is ambiguous for ${protocol}. Matches: ${candidates
67
+ .slice(0, 8)
68
+ .map((candidate) => candidate.id)
69
+ .join(', ')}`);
70
+ }
71
+ throw new SurfCliError('INVALID_USAGE', `No Surfshark ${protocol.toUpperCase()} profile matched "${query}". Run \`surf-cli servers list --protocol ${protocol}\`.`);
72
+ }
73
+ }
74
+ export function parseProfileFile(fileName, directory) {
75
+ const match = PROFILE_FILE_PATTERN.exec(fileName);
76
+ if (!match?.groups) {
77
+ return null;
78
+ }
79
+ const protocol = protocolSchema.parse(match.groups.protocol);
80
+ const locationCode = match.groups.locationCode;
81
+ const host = `${locationCode}.prod.surfshark.com`;
82
+ return {
83
+ id: `${locationCode}-${protocol}`,
84
+ host,
85
+ locationCode,
86
+ protocol,
87
+ fileName,
88
+ configPath: path.join(directory, fileName)
89
+ };
90
+ }
91
+ function assertSafeProfileName(entryName) {
92
+ const fileName = path.basename(entryName);
93
+ if (entryName !== fileName ||
94
+ entryName.includes('..') ||
95
+ entryName.includes('\0') ||
96
+ !PROFILE_FILE_PATTERN.test(fileName)) {
97
+ throw new SurfCliError('CONFIG_ERROR', `Surfshark configuration archive contained an unsafe or unexpected entry: ${entryName}`);
98
+ }
99
+ return fileName;
100
+ }
101
+ async function safeReadDir(directory) {
102
+ try {
103
+ return await readdir(directory);
104
+ }
105
+ catch (error) {
106
+ if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
107
+ return [];
108
+ }
109
+ throw error;
110
+ }
111
+ }
112
+ //# sourceMappingURL=server-catalog-service.js.map