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.
- package/LICENSE +202 -0
- package/README.md +233 -0
- package/dist/audit-log/chain-verifier.d.ts +9 -0
- package/dist/audit-log/chain-verifier.d.ts.map +1 -0
- package/dist/audit-log/chain-verifier.js +79 -0
- package/dist/audit-log/chain-verifier.js.map +1 -0
- package/dist/audit-log/hash-chain-writer.d.ts +16 -0
- package/dist/audit-log/hash-chain-writer.d.ts.map +1 -0
- package/dist/audit-log/hash-chain-writer.js +49 -0
- package/dist/audit-log/hash-chain-writer.js.map +1 -0
- package/dist/auth/credential-store.d.ts +24 -0
- package/dist/auth/credential-store.d.ts.map +1 -0
- package/dist/auth/credential-store.js +50 -0
- package/dist/auth/credential-store.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +74 -0
- package/dist/cli.js.map +1 -0
- package/dist/clients/reddit-client.d.ts +33 -0
- package/dist/clients/reddit-client.d.ts.map +1 -0
- package/dist/clients/reddit-client.js +153 -0
- package/dist/clients/reddit-client.js.map +1 -0
- package/dist/clients/youtube-client.d.ts +16 -0
- package/dist/clients/youtube-client.d.ts.map +1 -0
- package/dist/clients/youtube-client.js +77 -0
- package/dist/clients/youtube-client.js.map +1 -0
- package/dist/commands/auth.d.ts +8 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +90 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/search.d.ts +14 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +112 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/verify-log.d.ts +5 -0
- package/dist/commands/verify-log.d.ts.map +1 -0
- package/dist/commands/verify-log.js +19 -0
- package/dist/commands/verify-log.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/util/crypto.d.ts +16 -0
- package/dist/util/crypto.d.ts.map +1 -0
- package/dist/util/crypto.js +40 -0
- package/dist/util/crypto.js.map +1 -0
- package/dist/util/prompt.d.ts +11 -0
- package/dist/util/prompt.d.ts.map +1 -0
- package/dist/util/prompt.js +67 -0
- package/dist/util/prompt.js.map +1 -0
- 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 @@
|
|
|
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
|
package/dist/cli.js.map
ADDED
|
@@ -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 @@
|
|
|
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"}
|