Skip to content

Commit ae561a2

Browse files
author
cdavis-code
committed
yt_cli 2.3.0+1: security hardening, CLI example, doc improvements
- Add SecurityUtil for path validation, JSON schema validation, OAuth callback validation, and token file permissions - Replace example/yt_cli_example.dart with example/README.md showing CLI usage - Update CHANGELOG.md with current version - Add Activities API command - Update .pubignore to exclude doc/ and *.iml
1 parent 568d6c4 commit ae561a2

19 files changed

Lines changed: 482 additions & 30 deletions

packages/yt_cli/.pubignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
client_secret.json
22
youtube_server_tokens.json
33
.dart_tool/
4+
doc/
5+
*.iml

packages/yt_cli/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 2.3.0+1
4+
5+
### Changed
6+
- Security hardening: path validation, token file permissions, credential JSON schema validation, OAuth callback validation
7+
- example/ replaced with README.md showing CLI usage examples
8+
9+
## 2.3.0
10+
11+
### Added
12+
- Activities API command (`yt activities list`)
13+
14+
### Changed
15+
- Depends on yt ^2.3.0
16+
317
## 2.2.6+5
418

519
* publication readiness: LICENSE, .pubignore, topics, funding, version sync

packages/yt_cli/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ yt videos --help
6161
## Features
6262

6363
- OAuth 2.0 web flow authorization with automatic token refresh
64+
- Query channel activity events (uploads, likes, subscriptions, etc.)
6465
- Query YouTube Analytics reports (views, watch time, demographics, and more)
6566
- Manage analytics groups and group items for organizing channels, videos, or playlists
6667
- Full CRUD for channels, playlists, videos, comments, and subscriptions
@@ -76,6 +77,7 @@ Run `yt --help` for the full list. Key commands:
7677

7778
| Command | Description |
7879
|---------|-------------|
80+
| `activities` | List channel activity events |
7981
| `analytics` | YouTube Analytics reports, groups, and group items |
8082
| `authorize` | OAuth 2.0 web flow to authorize the CLI |
8183
| `broadcast` | Manage live broadcasts |
@@ -137,4 +139,4 @@ Any help from the open-source community is always welcome:
137139

138140
## License
139141

140-
MIT License - see [LICENSE] for details.
142+
MIT License - see [LICENSE](LICENSE) for details.

packages/yt_cli/bin/yt.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ void main(List<String> arguments) async {
1212
allowed: ['all', 'debug', 'info', 'warning', 'error', 'off'],
1313
defaultsTo: 'off',
1414
)
15+
..addCommand(YoutubeActivitiesCommand())
1516
..addCommand(YoutubeAnalyticsCommand())
1617
..addCommand(YoutubeAuthorizeCommand())
1718
..addCommand(YoutubeBroadcastCommand())

packages/yt_cli/example/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# yt_cli Examples
2+
3+
## Authorize the CLI
4+
5+
```sh
6+
# Download client_secret.json from Google Cloud Console
7+
yt authorize
8+
```
9+
10+
This opens a browser for OAuth 2.0 authorization. After approving, a refresh
11+
token is saved locally so subsequent runs are unattended.
12+
13+
## Common commands
14+
15+
```sh
16+
# Upload a video
17+
yt videos insert \
18+
--video-file my_video.mp4 \
19+
--part snippet \
20+
--notify-subscribers true \
21+
--body '{"snippet":{"title":"My Video","description":"Hello world!"}}'
22+
23+
# Set a custom thumbnail
24+
yt thumbnails set \
25+
--video-id YOUR_VIDEO_ID \
26+
--file thumbnail.jpg
27+
28+
# Search for videos
29+
yt search \
30+
--q "Dart programming" \
31+
--part snippet \
32+
--max-results 5
33+
34+
# List your channels
35+
yt channels list \
36+
--mine true \
37+
--part snippet
38+
```
39+
40+
Run `yt --help` or `yt <command> --help` for the full list of available
41+
commands and their options.

packages/yt_cli/lib/meta.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,5 @@ library;
66
import 'dart:convert' show json;
77

88
final pubSpec = json.decode(
9-
'{"name":"yt_cli","version":"2.2.6+6","description":"A CLI tool for the YouTube Data, Live Streaming, and Analytics APIs.","homepage":"https://github.com/cdavis-code/yt_workspace","repository":"https://github.com/cdavis-code/yt_workspace/tree/main/packages/yt_cli","issue_tracker":"https://github.com/cdavis-code/yt_workspace/issues"}',
9+
'{"name":"yt_cli","version":"2.3.0+1","description":"A CLI tool for the YouTube Data, Live Streaming, and Analytics APIs.","homepage":"https://github.com/cdavis-code/yt_workspace","repository":"https://github.com/cdavis-code/yt_workspace/tree/main/packages/yt_cli","issue_tracker":"https://github.com/cdavis-code/yt_workspace/issues"}',
1010
);
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import 'package:args/command_runner.dart';
2+
import 'package:dio/dio.dart';
3+
import 'package:yt/yt.dart';
4+
5+
import 'youtube_helper_command.dart';
6+
7+
/// An activity resource contains information about an action that a particular
8+
/// channel, or user, has taken on YouTube.
9+
class YoutubeActivitiesCommand extends Command<void> {
10+
@override
11+
String get description =>
12+
'An activity resource contains information about an action that a particular channel, or user, has taken on YouTube.';
13+
14+
@override
15+
String get name => 'activities';
16+
17+
YoutubeActivitiesCommand() {
18+
addSubcommand(YoutubeListActivitiesCommand());
19+
}
20+
}
21+
22+
/// Returns a list of channel activity events that match the request criteria.
23+
class YoutubeListActivitiesCommand extends YtHelperCommand {
24+
@override
25+
String get description =>
26+
'Returns a list of channel activity events that match the request criteria.';
27+
28+
@override
29+
String get name => 'list';
30+
31+
YoutubeListActivitiesCommand() {
32+
argParser
33+
..addOption(
34+
'part',
35+
defaultsTo: 'snippet,contentDetails',
36+
help:
37+
'The part parameter specifies the activity resource parts that the API response will include.',
38+
)
39+
..addOption(
40+
'channel-id',
41+
valueHelp: 'YouTube channel id',
42+
help:
43+
'The channelId parameter specifies a unique YouTube channel ID. The API will then return a list of that channel\'s activities.',
44+
)
45+
..addFlag(
46+
'mine',
47+
defaultsTo: false,
48+
negatable: false,
49+
help:
50+
'This parameter can only be used in a properly authorized request. Set this parameter\'s value to true to retrieve a feed of the authenticated user\'s activities.',
51+
)
52+
..addOption(
53+
'max-results',
54+
defaultsTo: '5',
55+
valueHelp: 'number',
56+
help:
57+
'The maxResults parameter specifies the maximum number of items that should be returned in the result set. Acceptable values are 1 to 50, inclusive. The default value is 5.',
58+
)
59+
..addOption(
60+
'page-token',
61+
valueHelp: 'token',
62+
help:
63+
'The pageToken parameter identifies a specific page in the result set that should be returned.',
64+
)
65+
..addOption(
66+
'published-after',
67+
valueHelp: 'date',
68+
help:
69+
'The publishedAfter parameter specifies the earliest date and time that an activity could have occurred for that activity to be included in the API response. The value is an RFC 3339 formatted date-time value (1970-01-01T00:00:00Z).',
70+
)
71+
..addOption(
72+
'published-before',
73+
valueHelp: 'date',
74+
help:
75+
'The publishedBefore parameter specifies the date and time before which an activity must have occurred for that activity to be included in the API response. The value is an RFC 3339 formatted date-time value (1970-01-01T00:00:00Z).',
76+
)
77+
..addOption(
78+
'region-code',
79+
valueHelp: 'code',
80+
help:
81+
'The regionCode parameter instructs the API to return results for the specified country. The parameter value is an ISO 3166-1 alpha-2 country code.',
82+
);
83+
}
84+
85+
@override
86+
void run() async {
87+
await initializeYt();
88+
89+
try {
90+
final response = await activities.list(
91+
part: argResults!['part'],
92+
channelId: argResults?['channel-id'],
93+
mine: argResults?.flag('mine') == true ? true : null,
94+
maxResults: int.tryParse(argResults?['max-results'] ?? ''),
95+
pageToken: argResults?['page-token'],
96+
publishedAfter: argResults?['published-after'],
97+
publishedBefore: argResults?['published-before'],
98+
regionCode: argResults?['region-code'],
99+
);
100+
101+
print(response);
102+
103+
close();
104+
} on DioException catch (err) {
105+
throw UsageException('API usage error:', err.usage);
106+
}
107+
}
108+
}

packages/yt_cli/lib/src/cmd/youtube_analytics_command.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import 'package:yt/yt.dart';
66

77
import 'youtube_helper_command.dart';
88

9+
/// Root command for YouTube Analytics operations.
10+
///
11+
/// Provides access to analytics reports, groups, and group items
12+
/// for measuring channel performance and audience engagement.
913
class YoutubeAnalyticsCommand extends Command<void> {
1014
@override
1115
String get description =>
@@ -23,6 +27,10 @@ class YoutubeAnalyticsCommand extends Command<void> {
2327

2428
// -- Reports --
2529

30+
/// Command group for querying YouTube Analytics reports.
31+
///
32+
/// Subcommands allow retrieving metrics like views, watch time,
33+
/// and audience demographics for channels or content owners.
2634
class YoutubeAnalyticsReportsCommand extends Command<void> {
2735
@override
2836
String get description => 'Query YouTube Analytics reports for a channel.';
@@ -35,6 +43,11 @@ class YoutubeAnalyticsReportsCommand extends Command<void> {
3543
}
3644
}
3745

46+
/// Query YouTube Analytics reports with custom metrics and dimensions.
47+
///
48+
/// Retrieves analytics data such as views, estimated minutes watched,
49+
/// subscribers gained, and other performance metrics for a specified
50+
/// time period and channel.
3851
class AnalyticsReportsQueryCommand extends YtHelperCommand {
3952
@override
4053
String get description =>
@@ -142,6 +155,10 @@ class AnalyticsReportsQueryCommand extends YtHelperCommand {
142155

143156
// -- Groups --
144157

158+
/// Command group for managing YouTube Analytics groups.
159+
///
160+
/// Analytics groups allow organizing channels, videos, playlists,
161+
/// or assets for consolidated reporting and analysis.
145162
class YoutubeAnalyticsGroupsCommand extends Command<void> {
146163
@override
147164
String get description =>
@@ -158,6 +175,10 @@ class YoutubeAnalyticsGroupsCommand extends Command<void> {
158175
}
159176
}
160177

178+
/// Retrieve a list of analytics groups for the authenticated user.
179+
///
180+
/// Lists all groups created by the authorized channel or content owner,
181+
/// with optional filtering by group type and pagination support.
161182
class GroupsListCommand extends YtHelperCommand {
162183
@override
163184
String get description =>

packages/yt_cli/lib/src/cmd/youtube_authorize_command.dart

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import 'dart:convert';
2-
31
import 'package:args/command_runner.dart';
42
import 'package:oauth2/oauth2.dart' as oauth2;
53
import 'package:universal_io/io.dart';
64

5+
import '../util/security_util.dart';
6+
77
/// Generate a refresh token used to authenticate the command line API
88
/// requests.
99
///
1010
/// Uses an OAuth 2.0 web flow that captures a permanent Refresh Token for
1111
/// unattended server-side operation. See the setup guide for details on
12-
/// obtaining a [client_secret.json] file from the Google Cloud Console.
12+
/// obtaining a `client_secret.json` file from the Google Cloud Console.
1313
class YoutubeAuthorizeCommand extends Command<void> {
1414
static const _guideUrl =
1515
'https://github.com/faithoflifedev/yt/blob/main/packages/yt_cli/authentication.md';
@@ -49,11 +49,16 @@ class YoutubeAuthorizeCommand extends Command<void> {
4949
final overwrite = argResults!['overwrite-credentials'] as bool;
5050
final credentialsFilePath = argResults!['credentials-file'] as String;
5151

52-
final secretFile = File(credentialsFilePath);
53-
if (!await secretFile.exists()) {
52+
final File secretFile;
53+
try {
54+
secretFile = await SecurityUtil.validateInputFile(
55+
credentialsFilePath,
56+
argName: 'credentials-file',
57+
);
58+
} on FormatException catch (e) {
5459
print(
55-
'Error: Could not find "$credentialsFilePath".\n'
56-
'Download it from the Google Cloud Console Credentials page.\n'
60+
'Error: ${e.message}\n'
61+
'Download client_secret.json from the Google Cloud Console Credentials page.\n'
5762
'See the setup guide above for step-by-step instructions.',
5863
);
5964
exit(1);
@@ -67,13 +72,16 @@ class YoutubeAuthorizeCommand extends Command<void> {
6772
return;
6873
}
6974

70-
// Parse the Google Cloud Console credentials JSON.
71-
final jsonString = await secretFile.readAsString();
72-
final Map<String, dynamic> parsedJson = jsonDecode(jsonString);
73-
74-
// The JSON wraps credentials under "web" or "installed".
75-
final String rootKey = parsedJson.containsKey('web') ? 'web' : 'installed';
76-
final Map<String, dynamic> config = parsedJson[rootKey];
75+
// Parse and validate the Google Cloud Console credentials JSON.
76+
final Map<String, dynamic> config;
77+
try {
78+
config = SecurityUtil.parseAndValidateClientSecret(
79+
await secretFile.readAsString(),
80+
);
81+
} on FormatException catch (e) {
82+
print('Error: ${e.message}');
83+
exit(1);
84+
}
7785

7886
final clientId = config['client_id'] as String;
7987
final clientSecret = config['client_secret'] as String;
@@ -124,6 +132,24 @@ class YoutubeAuthorizeCommand extends Command<void> {
124132
final request = await server.first;
125133
final callbackUri = request.uri;
126134

135+
// Validate the callback path matches the registered redirect URI and
136+
// that an authorization code was returned without an error.
137+
try {
138+
SecurityUtil.validateOAuthCallback(
139+
callback: callbackUri,
140+
expectedRedirect: redirectUrl,
141+
);
142+
} on FormatException catch (e) {
143+
request.response
144+
..statusCode = HttpStatus.badRequest
145+
..headers.contentType = ContentType.html
146+
..write('<h1>Authorization failed</h1><p>${e.message}</p>');
147+
await request.response.close();
148+
await server.close();
149+
print('Error: ${e.message}');
150+
exit(1);
151+
}
152+
127153
// Send a friendly success page back to the browser.
128154
request.response
129155
..statusCode = HttpStatus.ok
@@ -140,8 +166,10 @@ class YoutubeAuthorizeCommand extends Command<void> {
140166
callbackUri.queryParameters,
141167
);
142168

143-
// Persist credentials so subsequent runs are fully automatic.
169+
// Persist credentials so subsequent runs are fully automatic, then
170+
// restrict the token file to owner read/write only on POSIX.
144171
await tokenStorageFile.writeAsString(client.credentials.toJson());
172+
await SecurityUtil.restrictFilePermissions(tokenStorageFile);
145173

146174
print('');
147175
print('Authorization completed.');

0 commit comments

Comments
 (0)