auditreach-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 (55) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +233 -0
  3. package/dist/audit-log/chain-verifier.d.ts +9 -0
  4. package/dist/audit-log/chain-verifier.d.ts.map +1 -0
  5. package/dist/audit-log/chain-verifier.js +79 -0
  6. package/dist/audit-log/chain-verifier.js.map +1 -0
  7. package/dist/audit-log/hash-chain-writer.d.ts +16 -0
  8. package/dist/audit-log/hash-chain-writer.d.ts.map +1 -0
  9. package/dist/audit-log/hash-chain-writer.js +49 -0
  10. package/dist/audit-log/hash-chain-writer.js.map +1 -0
  11. package/dist/auth/credential-store.d.ts +24 -0
  12. package/dist/auth/credential-store.d.ts.map +1 -0
  13. package/dist/auth/credential-store.js +50 -0
  14. package/dist/auth/credential-store.js.map +1 -0
  15. package/dist/cli.d.ts +3 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +74 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/clients/reddit-client.d.ts +33 -0
  20. package/dist/clients/reddit-client.d.ts.map +1 -0
  21. package/dist/clients/reddit-client.js +153 -0
  22. package/dist/clients/reddit-client.js.map +1 -0
  23. package/dist/clients/youtube-client.d.ts +16 -0
  24. package/dist/clients/youtube-client.d.ts.map +1 -0
  25. package/dist/clients/youtube-client.js +77 -0
  26. package/dist/clients/youtube-client.js.map +1 -0
  27. package/dist/commands/auth.d.ts +8 -0
  28. package/dist/commands/auth.d.ts.map +1 -0
  29. package/dist/commands/auth.js +90 -0
  30. package/dist/commands/auth.js.map +1 -0
  31. package/dist/commands/search.d.ts +14 -0
  32. package/dist/commands/search.d.ts.map +1 -0
  33. package/dist/commands/search.js +112 -0
  34. package/dist/commands/search.js.map +1 -0
  35. package/dist/commands/verify-log.d.ts +5 -0
  36. package/dist/commands/verify-log.d.ts.map +1 -0
  37. package/dist/commands/verify-log.js +19 -0
  38. package/dist/commands/verify-log.js.map +1 -0
  39. package/dist/index.d.ts +8 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +7 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/types.d.ts +62 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +2 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/util/crypto.d.ts +16 -0
  48. package/dist/util/crypto.d.ts.map +1 -0
  49. package/dist/util/crypto.js +40 -0
  50. package/dist/util/crypto.js.map +1 -0
  51. package/dist/util/prompt.d.ts +11 -0
  52. package/dist/util/prompt.d.ts.map +1 -0
  53. package/dist/util/prompt.js +67 -0
  54. package/dist/util/prompt.js.map +1 -0
  55. package/package.json +62 -0
@@ -0,0 +1,50 @@
1
+ import { Entry } from "@napi-rs/keyring";
2
+ const SERVICE_NAME = "auditreach";
3
+ function accountName(platform, key) {
4
+ return `${platform}:${key}`;
5
+ }
6
+ /**
7
+ * All credential I/O goes through this module. It is the one place allowed
8
+ * to touch a raw secret -- callers get it back only to hand directly to an
9
+ * API client's auth constructor, never to log, print, or serialize it.
10
+ */
11
+ export function setCredential(platform, key, value) {
12
+ const entry = new Entry(SERVICE_NAME, accountName(platform, key));
13
+ entry.setPassword(value);
14
+ }
15
+ export function getCredential(platform, key) {
16
+ const entry = new Entry(SERVICE_NAME, accountName(platform, key));
17
+ try {
18
+ return entry.getPassword();
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export function deleteCredential(platform, key) {
25
+ const entry = new Entry(SERVICE_NAME, accountName(platform, key));
26
+ try {
27
+ return entry.deletePassword();
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ export function getRedditCredentials() {
34
+ const clientId = getCredential("reddit", "clientId");
35
+ const clientSecret = getCredential("reddit", "clientSecret");
36
+ const username = getCredential("reddit", "username");
37
+ const password = getCredential("reddit", "password");
38
+ if (!clientId || !clientSecret || !username || !password) {
39
+ return null;
40
+ }
41
+ return { clientId, clientSecret, username, password };
42
+ }
43
+ export function getYoutubeCredentials() {
44
+ const apiKey = getCredential("youtube", "apiKey");
45
+ if (!apiKey) {
46
+ return null;
47
+ }
48
+ return { apiKey };
49
+ }
50
+ //# sourceMappingURL=credential-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-store.js","sourceRoot":"","sources":["../../src/auth/credential-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAGzC,MAAM,YAAY,GAAG,YAAY,CAAC;AAMlC,SAAS,WAAW,CAAC,QAAkB,EAAE,GAAkB;IACzD,OAAO,GAAG,QAAQ,IAAI,GAAG,EAAE,CAAC;AAC9B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAAC,QAAkB,EAAE,GAAkB,EAAE,KAAa;IACjF,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,YAAY,EAAE,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IAClE,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,QAAkB,EAAE,GAAkB;IAClE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,YAAY,EAAE,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IAClE,IAAI,CAAC;QACH,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAkB,EAAE,GAAkB;IACrE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,YAAY,EAAE,WAAW,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IAClE,IAAI,CAAC;QACH,OAAO,KAAK,CAAC,cAAc,EAAE,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAaD,MAAM,UAAU,oBAAoB;IAClC,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACrD,MAAM,YAAY,GAAG,aAAa,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;IAC7D,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACrD,MAAM,QAAQ,GAAG,aAAa,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACrD,IAAI,CAAC,QAAQ,IAAI,CAAC,YAAY,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;QACzD,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC;AACxD,CAAC;AAED,MAAM,UAAU,qBAAqB;IACnC,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAClD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,CAAC;AACpB,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { runSearchCommand } from "./commands/search.js";
4
+ import { runAuthCommand } from "./commands/auth.js";
5
+ import { runVerifyLogCommand } from "./commands/verify-log.js";
6
+ import { DEFAULT_LIMIT as REDDIT_DEFAULT_LIMIT, MAX_LIMIT as REDDIT_MAX_LIMIT, } from "./clients/reddit-client.js";
7
+ import { MAX_MAX_RESULTS as YOUTUBE_MAX_MAX_RESULTS } from "./clients/youtube-client.js";
8
+ const VERSION = "0.1.0";
9
+ function assertPlatform(value) {
10
+ if (value !== "reddit" && value !== "youtube") {
11
+ console.error(`Unsupported platform "${value}". Supported in v0.1: reddit, youtube.`);
12
+ console.error("X (Twitter) support is deferred to v0.2 -- see README for why.");
13
+ process.exit(1);
14
+ }
15
+ }
16
+ const program = new Command();
17
+ program
18
+ .name("auditreach")
19
+ .description("Official-API-only, BYOK research CLI with a hash-chained compliance audit log")
20
+ .version(VERSION);
21
+ program
22
+ .command("search")
23
+ .description("Search a platform using its official API only")
24
+ .requiredOption("--platform <platform>", "reddit | youtube")
25
+ .option("--query <query>", "search query")
26
+ .option("--subreddit <subreddit>", "restrict search to one subreddit (reddit only)")
27
+ .option("--channel <handle>", "restrict search to one channel, e.g. @AnthropicAI (youtube only)")
28
+ .option("--since <date>", "only results published after this date, e.g. 2026-06-01 (youtube only)")
29
+ .option("--max-results <n>", `maximum results to return (default: ${REDDIT_DEFAULT_LIMIT}; platform caps: ${REDDIT_MAX_LIMIT} Reddit / ${YOUTUBE_MAX_MAX_RESULTS} YouTube)`, (v) => {
30
+ const parsed = parseInt(v, 10);
31
+ if (!Number.isFinite(parsed) || parsed <= 0) {
32
+ throw new Error(`--max-results must be a positive integer, got "${v}"`);
33
+ }
34
+ return parsed;
35
+ })
36
+ .option("--before <fullname>", "page results before this Reddit fullname cursor, e.g. t3_abc123 (reddit only)")
37
+ .option("--after <fullname>", "page results after this Reddit fullname cursor, e.g. t3_abc123 (reddit only)")
38
+ .option("--output <path>", "write full results JSON to this path")
39
+ .action(async (opts) => {
40
+ assertPlatform(opts.platform);
41
+ await runSearchCommand({
42
+ platform: opts.platform,
43
+ query: opts.query,
44
+ subreddit: opts.subreddit,
45
+ channel: opts.channel,
46
+ since: opts.since,
47
+ maxResults: opts.maxResults,
48
+ before: opts.before,
49
+ after: opts.after,
50
+ output: opts.output,
51
+ });
52
+ });
53
+ program
54
+ .command("auth")
55
+ .description("Set up or clear BYOK credentials for a platform (stored in your OS keychain)")
56
+ .requiredOption("--platform <platform>", "reddit | youtube")
57
+ .option("--clear", "delete stored credentials for this platform")
58
+ .option("--verify", "verify stored credentials are valid without running a search (no results file, no audit-log entry)")
59
+ .action(async (opts) => {
60
+ assertPlatform(opts.platform);
61
+ await runAuthCommand({ platform: opts.platform, clear: opts.clear, verify: opts.verify });
62
+ });
63
+ program
64
+ .command("verify-log")
65
+ .description("Verify the local hash-chained audit log has not been tampered with")
66
+ .option("--path <path>", "path to the audit log file")
67
+ .action(async (opts) => {
68
+ await runVerifyLogCommand({ path: opts.path });
69
+ });
70
+ program.parseAsync(process.argv).catch((error) => {
71
+ console.error(error instanceof Error ? error.message : String(error));
72
+ process.exitCode = 1;
73
+ });
74
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ import type { RedditCredentials } from "../auth/credential-store.js";
2
+ import type { RedditSearchOptions, SearchOutcome } from "../types.js";
3
+ export declare const DEFAULT_LIMIT = 25;
4
+ export declare const MAX_LIMIT = 100;
5
+ /**
6
+ * Talks to Reddit's official OAuth API only -- no cookie import, no session
7
+ * reuse. Uses the password grant (Reddit's "script app" flow), the
8
+ * documented mechanism for a single-user, read-only research tool.
9
+ */
10
+ export declare class RedditClient {
11
+ private readonly credentials;
12
+ private accessToken;
13
+ private tokenExpiresAt;
14
+ constructor(credentials: RedditCredentials);
15
+ private getAccessToken;
16
+ /**
17
+ * Builds a cause-specific suffix for a failed search request, mirroring
18
+ * the guidance style used for token-request failures above. Returns an
19
+ * empty string when no known cause can be diagnosed, so callers can
20
+ * always append the result directly onto the generic error message.
21
+ */
22
+ private diagnoseSearchFailure;
23
+ /**
24
+ * Performs the same OAuth token request used before every search, but
25
+ * without issuing a search call. Used by `auditreach auth --verify` to
26
+ * check credentials without requiring --query and without touching the
27
+ * results file or audit log. Always forces a fresh token request rather
28
+ * than reusing a cached one, so it reflects the current credential state.
29
+ */
30
+ verifyCredentials(): Promise<void>;
31
+ search(options: RedditSearchOptions): Promise<SearchOutcome>;
32
+ }
33
+ //# sourceMappingURL=reddit-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reddit-client.d.ts","sourceRoot":"","sources":["../../src/clients/reddit-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,aAAa,EAAoB,MAAM,aAAa,CAAC;AAQxF,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,SAAS,MAAM,CAAC;AA0C7B;;;;GAIG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoB;IAChD,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAK;gBAEf,WAAW,EAAE,iBAAiB;YAI5B,cAAc;IAoC5B;;;;;OAKG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;;;;;OAMG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAMlC,MAAM,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,aAAa,CAAC;CAiFnE"}
@@ -0,0 +1,153 @@
1
+ const USER_AGENT = "auditreach-cli/0.1.0 (official-API-only compliance research tool)";
2
+ // Applied silently when --max-results is omitted, and used as the hard
3
+ // ceiling even when --max-results is passed a larger value. Documented in
4
+ // --help (src/cli.ts) and README.md so a caller cannot be silently truncated
5
+ // without knowing more results exist -- see the truncation warning emitted
6
+ // by src/commands/search.ts.
7
+ export const DEFAULT_LIMIT = 25;
8
+ export const MAX_LIMIT = 100;
9
+ const TOKEN_URL = "https://www.reddit.com/api/v1/access_token";
10
+ const API_BASE = "https://oauth.reddit.com";
11
+ // Matches a subreddit value that still carries a leading "r/" or "/r/"
12
+ // prefix, e.g. "r/MachineLearning" or "/r/MachineLearning" instead of the
13
+ // bare "MachineLearning" the Reddit search API expects. This is the most
14
+ // common cause of an otherwise-undiagnosed 400 on the search endpoint (see
15
+ // https://github.com/praw-dev/praw/issues/1939).
16
+ const LEADING_SUBREDDIT_PREFIX = /^\/?r\//i;
17
+ /**
18
+ * Talks to Reddit's official OAuth API only -- no cookie import, no session
19
+ * reuse. Uses the password grant (Reddit's "script app" flow), the
20
+ * documented mechanism for a single-user, read-only research tool.
21
+ */
22
+ export class RedditClient {
23
+ credentials;
24
+ accessToken = null;
25
+ tokenExpiresAt = 0;
26
+ constructor(credentials) {
27
+ this.credentials = credentials;
28
+ }
29
+ async getAccessToken() {
30
+ if (this.accessToken && Date.now() < this.tokenExpiresAt) {
31
+ return this.accessToken;
32
+ }
33
+ const basicAuth = Buffer.from(`${this.credentials.clientId}:${this.credentials.clientSecret}`).toString("base64");
34
+ const response = await fetch(TOKEN_URL, {
35
+ method: "POST",
36
+ headers: {
37
+ Authorization: `Basic ${basicAuth}`,
38
+ "User-Agent": USER_AGENT,
39
+ "Content-Type": "application/x-www-form-urlencoded",
40
+ },
41
+ body: new URLSearchParams({
42
+ grant_type: "password",
43
+ username: this.credentials.username,
44
+ password: this.credentials.password,
45
+ }),
46
+ });
47
+ if (!response.ok) {
48
+ throw new Error(`Reddit OAuth token request failed: ${response.status} ${response.statusText}. Check your credentials with "auditreach auth --platform reddit".`);
49
+ }
50
+ const token = (await response.json());
51
+ this.accessToken = token.access_token;
52
+ // Refresh 60s before actual expiry to avoid a request failing mid-flight.
53
+ this.tokenExpiresAt = Date.now() + (token.expires_in - 60) * 1000;
54
+ return this.accessToken;
55
+ }
56
+ /**
57
+ * Builds a cause-specific suffix for a failed search request, mirroring
58
+ * the guidance style used for token-request failures above. Returns an
59
+ * empty string when no known cause can be diagnosed, so callers can
60
+ * always append the result directly onto the generic error message.
61
+ */
62
+ diagnoseSearchFailure(status, subreddit) {
63
+ if (status === 400 && subreddit && LEADING_SUBREDDIT_PREFIX.test(subreddit)) {
64
+ const cleaned = subreddit.replace(LEADING_SUBREDDIT_PREFIX, "");
65
+ return ` Your --subreddit value "${subreddit}" has a leading "r/" or "/r/" prefix -- Reddit's API expects just the subreddit name (try "${cleaned}" instead).`;
66
+ }
67
+ return "";
68
+ }
69
+ /**
70
+ * Performs the same OAuth token request used before every search, but
71
+ * without issuing a search call. Used by `auditreach auth --verify` to
72
+ * check credentials without requiring --query and without touching the
73
+ * results file or audit log. Always forces a fresh token request rather
74
+ * than reusing a cached one, so it reflects the current credential state.
75
+ */
76
+ async verifyCredentials() {
77
+ this.accessToken = null;
78
+ this.tokenExpiresAt = 0;
79
+ await this.getAccessToken();
80
+ }
81
+ async search(options) {
82
+ const limit = Math.min(options.limit ?? DEFAULT_LIMIT, MAX_LIMIT);
83
+ const accessToken = await this.getAccessToken();
84
+ const params = new URLSearchParams({
85
+ q: options.query,
86
+ sort: "relevance",
87
+ limit: String(limit),
88
+ });
89
+ if (options.subreddit) {
90
+ params.set("restrict_sr", "1");
91
+ }
92
+ // Reddit's search listing endpoint accepts standard Listing pagination
93
+ // cursors (before/after, Reddit "fullname" ids e.g. "t3_abc123"). These
94
+ // let a caller page past the ~1000-result search cap by walking the
95
+ // listing forward/backward from a known item instead of relying on
96
+ // offset-based paging, which Reddit's search API does not support.
97
+ if (options.before) {
98
+ params.set("before", options.before);
99
+ }
100
+ if (options.after) {
101
+ params.set("after", options.after);
102
+ }
103
+ const path = options.subreddit
104
+ ? `/r/${encodeURIComponent(options.subreddit)}/search`
105
+ : "/search";
106
+ const response = await fetch(`${API_BASE}${path}?${params.toString()}`, {
107
+ headers: {
108
+ Authorization: `Bearer ${accessToken}`,
109
+ "User-Agent": USER_AGENT,
110
+ },
111
+ });
112
+ if (!response.ok) {
113
+ throw new Error(`Reddit search request failed: ${response.status} ${response.statusText}.${this.diagnoseSearchFailure(response.status, options.subreddit)}`);
114
+ }
115
+ const listing = (await response.json());
116
+ const items = listing.data.children.map(({ data }) => ({
117
+ id: data.id,
118
+ title: data.title,
119
+ url: `https://reddit.com${data.permalink}`,
120
+ createdAt: new Date(data.created_utc * 1000).toISOString(),
121
+ author: data.author,
122
+ score: data.score,
123
+ extra: {
124
+ subreddit: data.subreddit_name_prefixed,
125
+ num_comments: data.num_comments,
126
+ },
127
+ }));
128
+ return {
129
+ platform: "reddit",
130
+ endpoint: options.subreddit ? `GET /r/${options.subreddit}/search` : "GET /search",
131
+ queryParams: {
132
+ query: options.query,
133
+ subreddit: options.subreddit,
134
+ limit,
135
+ before: options.before,
136
+ after: options.after,
137
+ },
138
+ authScope: "OAuth script-app grant, read-only, public-subreddit scope",
139
+ consentBasis: "Reddit API Terms -- public content, official API, read-only script-app credentials",
140
+ items,
141
+ // Cursors read back from Reddit's own response (not an echo of the
142
+ // request params above) -- feed `nextCursor.after` into the next
143
+ // call's `--after`/`options.after` to walk forward through results
144
+ // past the ~1000-result search cap, and `nextCursor.before` to walk
145
+ // backward. This is the piece praw#614 was actually stuck on.
146
+ nextCursor: {
147
+ after: listing.data.after ?? null,
148
+ before: listing.data.before ?? null,
149
+ },
150
+ };
151
+ }
152
+ }
153
+ //# sourceMappingURL=reddit-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reddit-client.js","sourceRoot":"","sources":["../../src/clients/reddit-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { YoutubeCredentials } from "../auth/credential-store.js";
2
+ import type { SearchOutcome, YoutubeSearchOptions } from "../types.js";
3
+ export declare const DEFAULT_MAX_RESULTS = 25;
4
+ export declare const MAX_MAX_RESULTS = 50;
5
+ export declare class YoutubeClient {
6
+ private readonly youtube;
7
+ constructor(credentials: YoutubeCredentials);
8
+ /**
9
+ * Issues a minimal authenticated request (1 quota unit, no query needed)
10
+ * to confirm the API key is valid. Used by `auditreach auth --verify`
11
+ * instead of running a real search.
12
+ */
13
+ verifyCredentials(): Promise<void>;
14
+ search(options: YoutubeSearchOptions): Promise<SearchOutcome>;
15
+ }
16
+ //# sourceMappingURL=youtube-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"youtube-client.d.ts","sourceRoot":"","sources":["../../src/clients/youtube-client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AACtE,OAAO,KAAK,EAAE,aAAa,EAAoB,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAOzF,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,eAAO,MAAM,eAAe,KAAK,CAAC;AAElC,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;gBAEhD,WAAW,EAAE,kBAAkB;IAI3C;;;;OAIG;IACG,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAIlC,MAAM,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC;CA2DpE"}
@@ -0,0 +1,77 @@
1
+ import { google } from "googleapis";
2
+ // Applied silently when --max-results is omitted, and used as the hard
3
+ // ceiling even when --max-results is passed a larger value. Documented in
4
+ // --help (src/cli.ts) and README.md so a caller cannot be silently truncated
5
+ // without knowing more results exist -- see the truncation warning emitted
6
+ // by src/commands/search.ts.
7
+ export const DEFAULT_MAX_RESULTS = 25;
8
+ export const MAX_MAX_RESULTS = 50;
9
+ export class YoutubeClient {
10
+ youtube;
11
+ constructor(credentials) {
12
+ this.youtube = google.youtube({ version: "v3", auth: credentials.apiKey });
13
+ }
14
+ /**
15
+ * Issues a minimal authenticated request (1 quota unit, no query needed)
16
+ * to confirm the API key is valid. Used by `auditreach auth --verify`
17
+ * instead of running a real search.
18
+ */
19
+ async verifyCredentials() {
20
+ await this.youtube.videoCategories.list({ part: ["snippet"], regionCode: "US" });
21
+ }
22
+ async search(options) {
23
+ if (!options.query && !options.channelHandle) {
24
+ throw new Error("YouTube search requires either --query or --channel");
25
+ }
26
+ const maxResults = Math.min(options.maxResults ?? DEFAULT_MAX_RESULTS, MAX_MAX_RESULTS);
27
+ let channelId;
28
+ if (options.channelHandle) {
29
+ const handle = options.channelHandle.startsWith("@")
30
+ ? options.channelHandle
31
+ : `@${options.channelHandle}`;
32
+ const channelResponse = await this.youtube.channels.list({
33
+ part: ["id"],
34
+ forHandle: handle,
35
+ });
36
+ channelId = channelResponse.data.items?.[0]?.id ?? undefined;
37
+ if (!channelId) {
38
+ throw new Error(`No YouTube channel found for handle "${handle}"`);
39
+ }
40
+ }
41
+ const response = await this.youtube.search.list({
42
+ part: ["snippet"],
43
+ q: options.query,
44
+ channelId,
45
+ publishedAfter: options.since ? new Date(options.since).toISOString() : undefined,
46
+ maxResults,
47
+ type: ["video"],
48
+ order: "date",
49
+ });
50
+ const items = (response.data.items ?? []).map((item) => ({
51
+ id: item.id?.videoId ?? "",
52
+ title: item.snippet?.title ?? "",
53
+ url: `https://www.youtube.com/watch?v=${item.id?.videoId ?? ""}`,
54
+ createdAt: item.snippet?.publishedAt ?? "",
55
+ author: item.snippet?.channelTitle ?? null,
56
+ score: null,
57
+ extra: {
58
+ channelId: item.snippet?.channelId,
59
+ description: item.snippet?.description,
60
+ },
61
+ }));
62
+ return {
63
+ platform: "youtube",
64
+ endpoint: "GET /youtube/v3/search",
65
+ queryParams: {
66
+ query: options.query,
67
+ channel: options.channelHandle,
68
+ since: options.since,
69
+ maxResults,
70
+ },
71
+ authScope: "YouTube Data API v3, API-key auth, public search scope",
72
+ consentBasis: "YouTube API Services Terms -- public content, official API, API-key auth",
73
+ items,
74
+ };
75
+ }
76
+ }
77
+ //# sourceMappingURL=youtube-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"youtube-client.js","sourceRoot":"","sources":["../../src/clients/youtube-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { Platform } from "../types.js";
2
+ export interface AuthCommandArgs {
3
+ platform: Platform;
4
+ clear?: boolean;
5
+ verify?: boolean;
6
+ }
7
+ export declare function runAuthCommand(args: AuthCommandArgs): Promise<void>;
8
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wBAAsB,cAAc,CAAC,IAAI,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAqCzE"}
@@ -0,0 +1,90 @@
1
+ import { setCredential, deleteCredential, getRedditCredentials, getYoutubeCredentials, } from "../auth/credential-store.js";
2
+ import { promptText, promptSecret } from "../util/prompt.js";
3
+ import { RedditClient } from "../clients/reddit-client.js";
4
+ import { YoutubeClient } from "../clients/youtube-client.js";
5
+ export async function runAuthCommand(args) {
6
+ if (args.clear) {
7
+ await clearCredentials(args.platform);
8
+ return;
9
+ }
10
+ if (args.verify) {
11
+ await verifyCredentials(args.platform);
12
+ return;
13
+ }
14
+ if (args.platform === "reddit") {
15
+ console.log("Setting up Reddit API credentials (OAuth script app).");
16
+ console.log('Create one at https://www.reddit.com/prefs/apps -- choose app type "script".\n');
17
+ const clientId = await promptText("Client ID: ");
18
+ const clientSecret = await promptSecret("Client secret: ");
19
+ const username = await promptText("Reddit username: ");
20
+ const password = await promptSecret("Reddit password: ");
21
+ setCredential("reddit", "clientId", clientId);
22
+ setCredential("reddit", "clientSecret", clientSecret);
23
+ setCredential("reddit", "username", username);
24
+ setCredential("reddit", "password", password);
25
+ console.log("\nReddit credentials stored in your OS keychain.");
26
+ console.log("Rate limits: Reddit's official API is generally workable for most research volumes.");
27
+ }
28
+ else {
29
+ console.log("Setting up YouTube Data API v3 credentials.");
30
+ console.log("Create an API key at https://console.cloud.google.com/apis/credentials\n");
31
+ const apiKey = await promptSecret("API key: ");
32
+ setCredential("youtube", "apiKey", apiKey);
33
+ console.log("\nYouTube credentials stored in your OS keychain.");
34
+ console.log("Rate limits: quota-based (10,000 units/day default), generally workable.");
35
+ }
36
+ }
37
+ async function clearCredentials(platform) {
38
+ if (platform === "reddit") {
39
+ deleteCredential("reddit", "clientId");
40
+ deleteCredential("reddit", "clientSecret");
41
+ deleteCredential("reddit", "username");
42
+ deleteCredential("reddit", "password");
43
+ }
44
+ else {
45
+ deleteCredential("youtube", "apiKey");
46
+ }
47
+ console.log(`Cleared stored credentials for ${platform}.`);
48
+ }
49
+ /**
50
+ * Standalone credential check for `auditreach auth --verify`. Reuses each
51
+ * client's token-fetch/auth-check logic (never duplicates it) and performs
52
+ * a single minimal authenticated request. Unlike `search`, this never
53
+ * requires --query, never writes a results file, and never appends an
54
+ * audit-log entry -- it only reports whether the stored credentials work.
55
+ */
56
+ async function verifyCredentials(platform) {
57
+ if (platform === "reddit") {
58
+ const credentials = getRedditCredentials();
59
+ if (!credentials) {
60
+ console.error('No Reddit credentials found. Run "auditreach auth --platform reddit" first.');
61
+ process.exitCode = 1;
62
+ return;
63
+ }
64
+ try {
65
+ await new RedditClient(credentials).verifyCredentials();
66
+ console.log("Reddit credentials are valid.");
67
+ }
68
+ catch (error) {
69
+ console.error(`Reddit credential check failed: ${error instanceof Error ? error.message : String(error)}`);
70
+ process.exitCode = 1;
71
+ }
72
+ }
73
+ else {
74
+ const credentials = getYoutubeCredentials();
75
+ if (!credentials) {
76
+ console.error('No YouTube credentials found. Run "auditreach auth --platform youtube" first.');
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ try {
81
+ await new YoutubeClient(credentials).verifyCredentials();
82
+ console.log("YouTube credentials are valid.");
83
+ }
84
+ catch (error) {
85
+ console.error(`YouTube credential check failed: ${error instanceof Error ? error.message : String(error)}`);
86
+ process.exitCode = 1;
87
+ }
88
+ }
89
+ }
90
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/commands/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { Platform } from "../types.js";
2
+ export interface SearchCommandArgs {
3
+ platform: Platform;
4
+ query?: string;
5
+ subreddit?: string;
6
+ channel?: string;
7
+ since?: string;
8
+ maxResults?: number;
9
+ before?: string;
10
+ after?: string;
11
+ output?: string;
12
+ }
13
+ export declare function runSearchCommand(args: SearchCommandArgs): Promise<void>;
14
+ //# sourceMappingURL=search.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/commands/search.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAiB,MAAM,aAAa,CAAC;AAE3D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,QAAQ,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmE7E"}