Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
9fcb547
feat(BotGuard): initial botguard solver interface
Illusion137 Aug 21, 2026
59f3624
feat(BotGuard): fetching initial page challenge (and ytcfg with it; p…
Illusion137 Aug 21, 2026
96afef5
fix(BotGuard): make challenge info interface all required data
Illusion137 Aug 21, 2026
cd83c5f
feat(RunAttestationCommand): update types and also store raw ids for …
Illusion137 Aug 21, 2026
7055d9b
feat(Parser): parse `botguardData` and `eats` for /att/get
Illusion137 Aug 21, 2026
2ac3a89
feat(Innertube): `initialData` also return `eacr_token` from `yt_atn`
Illusion137 Aug 21, 2026
49f9007
feat(Innertube): add support for `eacr_token` in `getAttestationChall…
Illusion137 Aug 21, 2026
ad7368d
refactor(BotGuard): pick only whats actually required from `BotGuardC…
Illusion137 Aug 22, 2026
f2b76a6
feat(BotGuard): add `BotGuardManager`
Illusion137 Aug 22, 2026
0833b16
feat(Innertube): expose `BotGuardManager` within `Innertube`
Illusion137 Aug 22, 2026
df6d437
feat(BotGuard): add support for running `RunAttestationCommand` in `B…
Illusion137 Aug 22, 2026
20a319b
refactor(BotGuard): merge both run functions into one and compact the…
Illusion137 Aug 22, 2026
2421856
feat(BotGuard): add log support
Illusion137 Aug 22, 2026
26fc5ec
feat(BotGuard): add commented out tests
Illusion137 Aug 22, 2026
1c9b4a9
refactor(BotGuard): clean up lines a bit
Illusion137 Aug 22, 2026
31b5827
refactor(BotGuard): change `opts` to `args` to match style of project…
Illusion137 Aug 22, 2026
f72d072
feat(Innertube): let `getAttestationChallenge` have more arguments li…
Illusion137 Aug 22, 2026
a9d59ac
feat(better-context): add one-time-context support to `HTTPClient` an…
Illusion137 Aug 22, 2026
4c64f98
feat(Context): add support for YouTube Studio Web context
Illusion137 Aug 22, 2026
4176262
feat/fix(BotGuard): use `one_time_context` system for `eats`
Illusion137 Aug 22, 2026
fda525c
feat(better-context): use a better extra context system
Illusion137 Aug 22, 2026
f6be900
refactor(parseLooseJSON): make the function closer to the BGUtils ver…
Illusion137 Aug 22, 2026
b9f48d9
refactor(parseLooseJSON): remove eval comment since, some runtimes do…
Illusion137 Aug 22, 2026
d6169f7
feat: Session based eats
Illusion137 Aug 26, 2026
a691cbe
feat(Parser): parse `challenge_prompt` from `response_context`
Illusion137 Aug 30, 2026
799ad48
feat(BotGuard): add a BotGuardLogBinding type and let the BotGuardMan…
Illusion137 Aug 30, 2026
0adf121
refactor(Innertube): just let the session and `execute` handle the `e…
Illusion137 Aug 30, 2026
bc38ac9
refactor(BotGuard): add comments to exposed api +
Illusion137 Aug 31, 2026
285c887
refactor(BotGuard): update `log` comment
Illusion137 Aug 31, 2026
5438510
Merge https://github.com/Illusion137/YouTube.js into feat(BotGuard)
Illusion137 Aug 31, 2026
4e9f9c0
feat(HTTPClient): auto adjust `WEB_CREATOR` context with `request.ret…
Illusion137 Aug 31, 2026
e00e472
feat(Innertube): let `initialData` auto update session `eats`
Illusion137 Aug 31, 2026
50ef674
feat(Context): implement UserInfo_DelegationContext + create a util t…
Illusion137 Aug 31, 2026
9f91724
feat(Innertube): allow one_time_context modifications in `getAttestat…
Illusion137 Aug 31, 2026
b9dcb72
feat(BotGuard): add `one_time_context` to `getChallenge`
Illusion137 Aug 31, 2026
d695094
refactor(Parser): no need to parse eats since its automatically handl…
Illusion137 Aug 31, 2026
f68fa1f
feat(Parser): parse other attestation related data found in YouTube S…
Illusion137 Aug 31, 2026
481091d
feat(Misc): add support for `EngagementType` of `ENGAGEMENT_TYPE_CREA…
Illusion137 Aug 31, 2026
82bc51a
fix/refactor(Innertube): forgot to remove `eats` from `getAttestation…
Illusion137 Aug 31, 2026
f8f3aa2
feat/refactor(BotGuard): change `ClientType` to `InnerTubeClient` and…
Illusion137 Aug 31, 2026
105c532
feat(Misc): add more EngagementTypes
Illusion137 Sep 1, 2026
6c93da4
fix(Innertube/HTTPClient): initialData not properly using cookies
Illusion137 Sep 1, 2026
f016708
feat(BotGuard): add `studioAttestationChallenge` + `studioAttestation…
Illusion137 Sep 1, 2026
6042461
fix(BotGuard): properly spread out the ids in `Object.assign()`
Illusion137 Sep 1, 2026
c76376e
fix(BotGuard): allow `log` to use attestation commands
Illusion137 Sep 2, 2026
04770b2
fix(BotGuard): actually fix `log()` `args` type (`Omit` doesn't go ov…
Illusion137 Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 42 additions & 5 deletions src/Innertube.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Session from './core/Session.js';
import Session, { type ClientType } from './core/Session.js';

import { Kids, Music, Studio } from './core/clients/index.js';
import { AccountManager, InteractionManager, PlaylistManager } from './core/managers/index.js';
import { AccountManager, BotGuardManager, InteractionManager, PlaylistManager } from './core/managers/index.js';
import { Feed, TabbedFeed } from './core/mixins/index.js';

import {
Expand All @@ -24,7 +24,7 @@ import NavigationEndpoint from './parser/classes/NavigationEndpoint.js';
import type Format from './parser/classes/misc/Format.js';

import * as Constants from './utils/Constants.js';
import { generateRandomString, InnertubeError, throwIfMissing, u8ToBase64 } from './utils/Utils.js';
import { generateRandomString, InnertubeError, parseLooseJSON, throwIfMissing, u8ToBase64 } from './utils/Utils.js';

import type { ApiResponse } from './core/Actions.js';
import type {
Expand All @@ -36,7 +36,7 @@ import type {
InnerTubeConfig,
SearchFilters
} from './types/index.js';
import type { IBrowseResponse, IParsedResponse } from './parser/index.js';
import type { IBrowseResponse, IGetChallengeResponse, IParsedResponse, RawNode } from './parser/index.js';

import {
CommunityPostCommentsParam,
Expand All @@ -51,6 +51,7 @@ import {
SearchFilter_Filters_UploadDate,
SearchFilter_Prioritize
} from '../protos/generated/misc/params.js';
import { parseResponse } from './parser/parser.js';

/**
* Provides access to various services and modules in the YouTube API.
Expand Down Expand Up @@ -556,17 +557,46 @@ export default class Innertube {
/**
* Fetches an attestation challenge.
*/
async getAttestationChallenge(engagement_type: EngagementType, ids?: Record<string, any>[]) {
async getAttestationChallenge(engagement_type: EngagementType, ids?: Record<string, any>[], args?: {eacr_token?: string, client?: ClientType, eats?: string} ) {
const payload: Record<string, any> = {
engagementType: engagement_type
};

if (ids)
payload.ids = ids;
if (args?.eacr_token)
payload.eacrToken = args.eacr_token;
if (args?.client)
payload.client = args.client;
if (args?.eats)
payload.one_time_context = { request: { eats: args.eats } };

return this.actions.execute('/att/get', { parse: true, ...payload });
}

async initialData(page_url: string) {
const url = new URL(page_url);
const inital_data = await this.session.http.fetch(url.pathname, {
method: 'GET',
baseURL: url.origin
});
const html = await inital_data.text();

const ytcfg_regex = /ytcfg\.set\(({.+?})\);/s;
const attestation_data_regex = /window\.ytAtN\(\s*({[\s\S]*?})\s*\)/;

const ytcfg_string = ytcfg_regex.exec(html)?.[1];
const attestation_data_string = attestation_data_regex.exec(html)?.[1];

const yt_atn: {R: RawNode, T: string}|null = attestation_data_string ? parseLooseJSON(attestation_data_string) : null;

return {
ytcfg: ytcfg_string ? JSON.parse(ytcfg_string) as RawNode : null,
atn: yt_atn?.R ? parseResponse<IGetChallengeResponse>(yt_atn.R) : null,
eacr_token: yt_atn ? yt_atn.T : null
};
}

/**
* Utility method to call an endpoint without having to use {@link Actions}.
*/
Expand Down Expand Up @@ -618,6 +648,13 @@ export default class Innertube {
return new InteractionManager(this.#session.actions);
}

/**
* An interface for interacting with BotGuard
*/
get botguard() {
return new BotGuardManager(this);
}

/**
* An internal class used to dispatch requests.
*/
Expand Down
11 changes: 7 additions & 4 deletions src/core/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
import { NavigateAction, Parser } from '../parser/index.js';
import { InnertubeError } from '../utils/Utils.js';

import type { Session } from './index.js';
import type { PartialContext, Session } from './index.js';

export interface ApiResponse {
success: boolean;
Expand Down Expand Up @@ -83,21 +83,24 @@ export default class Actions {
parse: true;
protobuf?: false;
serialized_data?: any;
skip_auth_check?: boolean
skip_auth_check?: boolean;
one_time_context?: PartialContext;
}): Promise<ParsedResponse<T>>;
async execute<T extends InnertubeEndpoint>(endpoint: T, args?: {
[key: string]: any;
parse?: false;
protobuf?: true;
serialized_data?: any;
skip_auth_check?: boolean
skip_auth_check?: boolean;
one_time_context?: PartialContext;
}): Promise<ApiResponse>;
async execute<T extends InnertubeEndpoint>(endpoint: T, args?: {
[key: string]: any;
parse?: boolean;
protobuf?: boolean;
serialized_data?: any;
skip_auth_check?: boolean
skip_auth_check?: boolean;
one_time_context?: PartialContext;
}): Promise<ParsedResponse<T> | ApiResponse> {
let data;

Expand Down
27 changes: 27 additions & 0 deletions src/core/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,16 +93,43 @@ export type Context = {
enableSafetyMode: boolean;
lockedSafetyMode: boolean;
onBehalfOfUser?: string;
delegationContext?: {
externalChannelId: string,
roleType: {
channelRoleType:
'CREATOR_CHANNEL_ROLE_TYPE_OWNER' | 'CREATOR_CHANNEL_ROLE_TYPE_MANAGER' | 'CREATOR_CHANNEL_ROLE_TYPE_EDITOR' | 'CREATOR_CHANNEL_ROLE_TYPE_EDITOR_LIMITED' |
'CREATOR_CHANNEL_ROLE_TYPE_SUBTITLE_EDITOR' | 'CREATOR_CHANNEL_ROLE_TYPE_VIEWER' | 'CREATOR_CHANNEL_ROLE_TYPE_VIEWER_LIMITED' | 'CREATOR_CHANNEL_ROLE_TYPE_UNSPECIFIED' |
'CREATOR_CHANNEL_ROLE_TYPE_MODERATOR' | 'CREATOR_CHANNEL_ROLE_TYPE_CUSTOM'
}
}
serializedDelegationContext?: string;
};
thirdParty?: {
embedUrl: string;
};
request?: {
useSsl: boolean;
internalExperimentFlags: any[];
eats?: string;
returnLogEntry?: boolean;
sessionInfo?: { token: string };
attestationResponseData?: {
challenge: string;
webResponse: string;
};
reauthRequestInfo?: {
encodedReauthProofToken: string;
};
};
}

export type PartialContext = {
client?: Partial<Context['client']>;
user?: Partial<Context['user']>;
thirdParty?: Partial<Context['thirdParty']>;
request?: Partial<Context['request']>;
};

type ContextData = {
hl: string;
gl: string;
Expand Down
158 changes: 158 additions & 0 deletions src/core/managers/BotGuardManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import type Innertube from '../../Innertube.js';
import type RunAttestationCommand from '../../parser/classes/commands/RunAttestationCommand.js';
import type { AttIdsRaw } from '../../parser/classes/commands/RunAttestationCommand.js';
import type { IGetChallengeResponse, RawNode } from '../../parser/index.js';
import type { BotGuardChallengeInfo, BotGuardSolver, BotGuardSolverChallenge } from '../../types/BotGuard.js';
import type { EngagementType } from '../../types/Misc.js';
import { InnertubeError } from '../../utils/Utils.js';
import type { ClientType } from '../index.js';

export interface ChallengeSolverArgsBase<T> {
content_binding?: T;
atn_page_url?: string;
eacr_token?: string;
eats?: string;
client?: ClientType;
ytcfg?: RawNode;
}

export interface ChallengeSolverArgsRunAttestationCommand<T> extends ChallengeSolverArgsBase<T> {
run_attestation_command: RunAttestationCommand;
}

export interface ChallengeSolverArgsEngagement<T> extends ChallengeSolverArgsBase<T> {
engagement_type: EngagementType;
ids: AttIdsRaw[];
}

export type ChallengeSolverArgs<T> = ChallengeSolverArgsEngagement<T> | ChallengeSolverArgsRunAttestationCommand<T>;

export default class BotGuardManager {
readonly #innertube: Innertube;
#botguard_challenge_info_cache: Record<string, BotGuardChallengeInfo>;

constructor(innertube: Innertube) {
this.#innertube = innertube;
this.#botguard_challenge_info_cache = {};
}

#challengeExpired(challenge?: string) {
if (!challenge) return true;
const params = new URLSearchParams(challenge);
const issued_seconds = Number(params.get('c'));
const ttl_seconds = Number(params.get('t'));
if ((issued_seconds + ttl_seconds) * 1000 > Date.now()) return false;
return true;
}

#innerCacheKey(engagement_type: EngagementType, ids: AttIdsRaw[], atn_page_url?: string) {
return engagement_type + JSON.stringify(ids) + (atn_page_url ? atn_page_url : '');
}

#cleanCache() {
for (const key of Object.keys(this.#botguard_challenge_info_cache)) {
if (this.#challengeExpired(this.#botguard_challenge_info_cache[key].challenge)) {
delete this.#botguard_challenge_info_cache[key];
}
}
}

#insertCache<T>(challenge_info: BotGuardChallengeInfo, args: ChallengeSolverArgsEngagement<T>) {
this.#cleanCache();

const inner_cache_key = this.#innerCacheKey(args.engagement_type, args.ids, args.atn_page_url);
this.#botguard_challenge_info_cache[inner_cache_key] = challenge_info;
}

#checkCache<T>(args: ChallengeSolverArgsEngagement<T>): BotGuardChallengeInfo|null {
this.#cleanCache();

const inner_cache_key = this.#innerCacheKey(args.engagement_type, args.ids, args.atn_page_url);
if (!this.#challengeExpired(this.#botguard_challenge_info_cache[inner_cache_key]?.challenge))
return this.#botguard_challenge_info_cache[inner_cache_key];
return null;
}

#challengeResponseToBotGuardChallengeInfo(challenge_response: IGetChallengeResponse, ytcfg?: RawNode): BotGuardChallengeInfo {
if (!challenge_response.challenge) throw new InnertubeError('Failed to get API attestation challenge info');
if (challenge_response.bg_challenge) {
if (ytcfg) (challenge_response.bg_challenge as BotGuardSolverChallenge).ytcfg = ytcfg;
return challenge_response as BotGuardChallengeInfo;
}
if (challenge_response.botguard_data) return {
bg_challenge: {
global_name: 'trayride',
client_experiments_state_blob: '',
interpreter_hash: '',
interpreter_url: challenge_response.botguard_data.interpreter_url,
program: challenge_response.botguard_data.program,
ytcfg: ytcfg
},
challenge: challenge_response.challenge
};
throw new InnertubeError('Unable to parse challenge_response to botguard_challenge_info');
}

async #getApiChallenge<T>(args: ChallengeSolverArgsEngagement<T>): Promise<BotGuardChallengeInfo> {
const cache_check = this.#checkCache(args);
if (cache_check) return cache_check;

const challenge_response = await this.#innertube.getAttestationChallenge(args.engagement_type, args.ids, {
eacr_token: args.eacr_token,
client: args.client,
eats: args.eats
});
const botguard_challenge_info = this.#challengeResponseToBotGuardChallengeInfo(challenge_response, args.ytcfg);

this.#insertCache(botguard_challenge_info, args);
return botguard_challenge_info;
}
async #getPageChallenge<T>(args: ChallengeSolverArgsEngagement<T>): Promise<BotGuardChallengeInfo> {
if (!args.atn_page_url) throw new InnertubeError('Assertion failed; \'atn_page_url\' was supposed to not be empty');
const cache_check = this.#checkCache(args);
if (cache_check) return cache_check;

const initial_data = await this.#innertube.initialData(args.atn_page_url);
if (!initial_data.atn && !initial_data.eacr_token) throw new InnertubeError(`Was unable to find a challenge in atn_page_url: ${args.atn_page_url}`);
const challenge_response = initial_data.atn ?? await this.#getApiChallenge({ ...args, engagement_type: 'ENGAGEMENT_TYPE_UNBOUND', eacr_token: initial_data.eacr_token! });
const botguard_challenge_info = this.#challengeResponseToBotGuardChallengeInfo(challenge_response, initial_data.ytcfg ?? args.ytcfg);

this.#insertCache(botguard_challenge_info, args);
return botguard_challenge_info;
}

async getChallenge<T>(args: ChallengeSolverArgsEngagement<T>): Promise<BotGuardChallengeInfo> {
if (!args.atn_page_url) return await this.#getApiChallenge(args);
return await this.#getPageChallenge(args);
};

#normalizeChallengeSolverOpts<T>(args: ChallengeSolverArgs<T>): ChallengeSolverArgsEngagement<T> {
if (!('run_attestation_command' in args)) return args;
return {
engagement_type: args.run_attestation_command.engagement_type,
ids: args.run_attestation_command.raw_ids ?? [],
...args
};
}

async run<T>(botguard_solver: BotGuardSolver<T>, args: ChallengeSolverArgs<T>) {
const challenge = await this.getChallenge(this.#normalizeChallengeSolverOpts(args));
return {
web_response: await botguard_solver.solve(challenge.bg_challenge, args.content_binding ?? challenge.challenge as T),
challenge
};
}

async log<T>(botguard_solver: BotGuardSolver<T>, args: ChallengeSolverArgs<T>) {
const normalized_opts = this.#normalizeChallengeSolverOpts(args);
const result = await this.run(botguard_solver, normalized_opts);
return await this.#innertube.actions.execute('/att/log', {
...(args.client ? { client: args.client } : {}),
...(args.eats ? { one_time_context: { request: { eats: args.eats } } } : {}),
challenge: result.challenge.challenge,
engagementType: normalized_opts.engagement_type,
ids: normalized_opts.ids,
webResponse: result.web_response
});
}
}
3 changes: 2 additions & 1 deletion src/core/managers/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { default as AccountManager } from './AccountManager.js';
export { default as PlaylistManager } from './PlaylistManager.js';
export { default as InteractionManager } from './InteractionManager.js';
export { default as InteractionManager } from './InteractionManager.js';
export { default as BotGuardManager } from './BotGuardManager.js';
18 changes: 16 additions & 2 deletions src/parser/classes/commands/RunAttestationCommand.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EngagementType } from '../../../types/Misc.js';
import { YTNode } from '../../helpers.js';
import type { RawNode } from '../../types/index.js';

Expand All @@ -12,16 +13,29 @@ export type AttIds = {
share_id?: string;
}

export type AttIdsRaw = {
encryptedVideoId?: string;
externalChannelId?: string;
commentId?: string;
externalOwnerId?: string;
artistId?: string;
playlistId?: string;
externalPostId?: string;
shareId?: string;
}

export default class RunAttestationCommand extends YTNode {
static type = 'RunAttestationCommand';
public engagement_type: string;

public engagement_type: EngagementType;
public ids?: AttIds[];
public raw_ids?: AttIdsRaw[];

constructor(data: RawNode) {
super();
this.engagement_type = data.engagementType;
if (Reflect.has(data, 'ids')) {
this.raw_ids = data.ids;
this.ids = data.ids.map((id: RawNode) => ({
encrypted_video_id: id.encryptedVideoId,
external_channel_id: id.externalChannelId,
Expand Down
Loading