kaizoku-core 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 +0 -0
- package/README.md +105 -0
- package/dist/extractors/kwik.d.ts +6 -0
- package/dist/extractors/kwik.js +33 -0
- package/dist/extractors/megaplay.d.ts +2 -0
- package/dist/extractors/megaplay.js +32 -0
- package/dist/extractors/streamwish.d.ts +4 -0
- package/dist/extractors/streamwish.js +86 -0
- package/dist/extractors/vidtube.d.ts +2 -0
- package/dist/extractors/vidtube.js +24 -0
- package/dist/extractors/vidwish.d.ts +2 -0
- package/dist/extractors/vidwish.js +32 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +18 -0
- package/dist/lib/config.d.ts +22 -0
- package/dist/lib/config.js +21 -0
- package/dist/providers/anime/anidb.d.ts +6 -0
- package/dist/providers/anime/anidb.js +88 -0
- package/dist/providers/anime/anikoto.d.ts +41 -0
- package/dist/providers/anime/anikoto.js +259 -0
- package/dist/providers/anime/animegg.d.ts +6 -0
- package/dist/providers/anime/animegg.js +107 -0
- package/dist/providers/anime/animeonsen.d.ts +6 -0
- package/dist/providers/anime/animeonsen.js +95 -0
- package/dist/providers/anime/animepahe.d.ts +6 -0
- package/dist/providers/anime/animepahe.js +102 -0
- package/dist/providers/anime/animesaturn.d.ts +8 -0
- package/dist/providers/anime/animesaturn.js +160 -0
- package/dist/providers/anime/animeunity.d.ts +4 -0
- package/dist/providers/anime/animeunity.js +108 -0
- package/dist/providers/anime/anizone.d.ts +12 -0
- package/dist/providers/anime/anizone.js +146 -0
- package/dist/providers/anime/gojo.d.ts +6 -0
- package/dist/providers/anime/gojo.js +83 -0
- package/dist/providers/meta/anilist/anilist.d.ts +28 -0
- package/dist/providers/meta/anilist/anilist.js +263 -0
- package/dist/providers/meta/anilist/queries.d.ts +22 -0
- package/dist/providers/meta/anilist/queries.js +405 -0
- package/dist/providers/meta/anilist/types.d.ts +213 -0
- package/dist/providers/meta/anilist/types.js +21 -0
- package/dist/providers/meta/anilist.d.ts +15 -0
- package/dist/providers/meta/anilist.js +94 -0
- package/dist/types/types.d.ts +88 -0
- package/dist/types/types.js +4 -0
- package/dist/utils/http.d.ts +13 -0
- package/dist/utils/http.js +39 -0
- package/dist/utils/proxy.d.ts +9 -0
- package/dist/utils/proxy.js +43 -0
- package/dist/utils/shared.d.ts +15 -0
- package/dist/utils/shared.js +64 -0
- package/dist/utils/unpack.d.ts +9 -0
- package/dist/utils/unpack.js +59 -0
- package/package.json +34 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { fetchApi } from '../../utils/http.js';
|
|
2
|
+
import * as anizone from '../anime/anizone.js';
|
|
3
|
+
import * as anikoto from '../anime/anikoto.js';
|
|
4
|
+
import * as animeonsen from '../anime/animeonsen.js';
|
|
5
|
+
// Add other providers as needed
|
|
6
|
+
const ANIZIP_BASE = 'https://api.ani.zip/mappings?anilist_id=';
|
|
7
|
+
const ANILIST_GRAPHQL = 'https://graphql.anilist.co';
|
|
8
|
+
/**
|
|
9
|
+
* The available providers in the meta wrapper.
|
|
10
|
+
*/
|
|
11
|
+
export const PROVIDERS = {
|
|
12
|
+
anizone,
|
|
13
|
+
anikoto,
|
|
14
|
+
animeonsen
|
|
15
|
+
};
|
|
16
|
+
export async function getAniZipData(anilistId) {
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetchApi(`${ANIZIP_BASE}${anilistId}`);
|
|
19
|
+
if (!res.ok)
|
|
20
|
+
return null;
|
|
21
|
+
return await res.json();
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Tries to find the anime on a specific provider by searching its titles.
|
|
29
|
+
*/
|
|
30
|
+
export async function mapToProvider(anilistId, providerName) {
|
|
31
|
+
const anizip = await getAniZipData(anilistId);
|
|
32
|
+
if (!anizip)
|
|
33
|
+
return null;
|
|
34
|
+
const provider = PROVIDERS[providerName.toLowerCase()];
|
|
35
|
+
if (!provider) {
|
|
36
|
+
throw new Error(`Provider ${providerName} not found.`);
|
|
37
|
+
}
|
|
38
|
+
// Try english title first, then romaji
|
|
39
|
+
const titlesToTry = [
|
|
40
|
+
anizip.titles.english,
|
|
41
|
+
anizip.titles.romaji,
|
|
42
|
+
// Remove special characters for better search
|
|
43
|
+
anizip.titles.english?.replace(/[^a-zA-Z0-9 ]/g, ''),
|
|
44
|
+
anizip.titles.romaji?.replace(/[^a-zA-Z0-9 ]/g, '')
|
|
45
|
+
].filter(Boolean);
|
|
46
|
+
for (const title of titlesToTry) {
|
|
47
|
+
try {
|
|
48
|
+
const results = await provider.search(title);
|
|
49
|
+
if (results && results.length > 0) {
|
|
50
|
+
// To be safe, we could use string similarity here, but for now we take the first match
|
|
51
|
+
return results[0].id;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
console.warn(`Failed to search provider ${providerName} for title: ${title}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Fetches episodes from a provider and enriches them with AniZip metadata (titles, images, filler).
|
|
62
|
+
*/
|
|
63
|
+
export async function fetchEpisodes(anilistId, providerName) {
|
|
64
|
+
const providerId = await mapToProvider(anilistId, providerName);
|
|
65
|
+
if (!providerId) {
|
|
66
|
+
throw new Error(`Could not map AniList ID ${anilistId} to ${providerName}`);
|
|
67
|
+
}
|
|
68
|
+
const provider = PROVIDERS[providerName.toLowerCase()];
|
|
69
|
+
const anizip = await getAniZipData(anilistId);
|
|
70
|
+
const info = await provider.fetchAnimeInfo(providerId);
|
|
71
|
+
const enrichedEpisodes = info.episodes.map(ep => {
|
|
72
|
+
// Enriched with AniZip data if available
|
|
73
|
+
const azEp = anizip?.episodes?.[ep.number.toString()];
|
|
74
|
+
return {
|
|
75
|
+
...ep,
|
|
76
|
+
title: azEp?.title_en || azEp?.title_ro || azEp?.title || ep.title,
|
|
77
|
+
description: azEp?.overview || ep.description,
|
|
78
|
+
image: azEp?.image || ep.image,
|
|
79
|
+
isFiller: azEp?.isFiller ?? ep.isFiller,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
return enrichedEpisodes;
|
|
83
|
+
}
|
|
84
|
+
export async function fetchEpisodeSources(episodeId, providerName, extraArgs) {
|
|
85
|
+
const provider = PROVIDERS[providerName.toLowerCase()];
|
|
86
|
+
if (!provider) {
|
|
87
|
+
throw new Error(`Provider ${providerName} not found.`);
|
|
88
|
+
}
|
|
89
|
+
// Pass extraArgs (like animeId for AnimePahe) if the provider needs it
|
|
90
|
+
if (extraArgs) {
|
|
91
|
+
return provider.fetchEpisodeSources(extraArgs, episodeId);
|
|
92
|
+
}
|
|
93
|
+
return provider.fetchEpisodeSources(episodeId);
|
|
94
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types every extractor (and, later, every provider) normalizes into.
|
|
3
|
+
*/
|
|
4
|
+
export interface Source {
|
|
5
|
+
url: string;
|
|
6
|
+
/** Always derived by checking the url itself — no extractor sources this independently. */
|
|
7
|
+
isM3U8: boolean;
|
|
8
|
+
/** e.g. "1080p", "multi-quality", "single", "default" — free text, providers don't agree on a fixed vocabulary. */
|
|
9
|
+
quality: string;
|
|
10
|
+
type?: "hls" | "mp4" | "dash" | "unknown";
|
|
11
|
+
proxiedUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Subtitle {
|
|
14
|
+
url: string;
|
|
15
|
+
lang: string;
|
|
16
|
+
isDefault?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** Populated by Anizone (chapters/thumbnails tracks) — most providers leave this empty. */
|
|
19
|
+
export interface Track {
|
|
20
|
+
url: string;
|
|
21
|
+
type: string;
|
|
22
|
+
}
|
|
23
|
+
export interface VideoStream {
|
|
24
|
+
sources: Source[];
|
|
25
|
+
subtitles: Subtitle[];
|
|
26
|
+
tracks?: Track[];
|
|
27
|
+
intro?: {
|
|
28
|
+
start: number;
|
|
29
|
+
end: number;
|
|
30
|
+
};
|
|
31
|
+
outro?: {
|
|
32
|
+
start: number;
|
|
33
|
+
end: number;
|
|
34
|
+
};
|
|
35
|
+
download?: string;
|
|
36
|
+
/** Referer / Origin / User-Agent — whatever this specific stream needs to actually play. */
|
|
37
|
+
headers?: Record<string, string>;
|
|
38
|
+
}
|
|
39
|
+
export type ProviderName = 'anikoto' | 'anizone' | 'animeonsen' | 'animeunity' | 'gojo' | 'animegg' | 'anidb' | 'animesaturn' | 'animepahe';
|
|
40
|
+
export interface Episode {
|
|
41
|
+
id: string;
|
|
42
|
+
number: number;
|
|
43
|
+
title?: string;
|
|
44
|
+
image?: string;
|
|
45
|
+
airDate?: string;
|
|
46
|
+
hasDub?: boolean;
|
|
47
|
+
hasSub?: boolean;
|
|
48
|
+
/** we need to use anizip data here to satisfy this */
|
|
49
|
+
filler?: boolean;
|
|
50
|
+
/**most providers dont have seconds so we kinda need to use anizip here */
|
|
51
|
+
duration?: number;
|
|
52
|
+
description?: string;
|
|
53
|
+
providerName?: ProviderName;
|
|
54
|
+
}
|
|
55
|
+
export interface ProviderStats {
|
|
56
|
+
name: string;
|
|
57
|
+
baseUrl: string;
|
|
58
|
+
isWorking: boolean;
|
|
59
|
+
}
|
|
60
|
+
export interface AnimeResult {
|
|
61
|
+
id: string;
|
|
62
|
+
title: string | {
|
|
63
|
+
romaji?: string;
|
|
64
|
+
english?: string;
|
|
65
|
+
native?: string;
|
|
66
|
+
userPreferred?: string;
|
|
67
|
+
};
|
|
68
|
+
url?: string;
|
|
69
|
+
image?: string;
|
|
70
|
+
releaseDate?: string;
|
|
71
|
+
type?: string;
|
|
72
|
+
status?: string;
|
|
73
|
+
[key: string]: any;
|
|
74
|
+
}
|
|
75
|
+
export interface AnimeInfo extends AnimeResult {
|
|
76
|
+
description?: string;
|
|
77
|
+
genres?: string[];
|
|
78
|
+
totalEpisodes?: number;
|
|
79
|
+
episodes: Episode[];
|
|
80
|
+
[key: string]: any;
|
|
81
|
+
}
|
|
82
|
+
export interface PageResult<T> {
|
|
83
|
+
currentPage?: number;
|
|
84
|
+
hasNextPage?: boolean;
|
|
85
|
+
totalPages?: number;
|
|
86
|
+
totalResults?: number;
|
|
87
|
+
results: T[];
|
|
88
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal fetch wrapper shared by every extractor/provider.
|
|
3
|
+
* Uses Node's global fetch directly — no extra HTTP dependency.
|
|
4
|
+
*/
|
|
5
|
+
export declare class HttpError extends Error {
|
|
6
|
+
readonly status: number;
|
|
7
|
+
readonly statusText: string;
|
|
8
|
+
readonly url: string;
|
|
9
|
+
constructor(status: number, statusText: string, url: string);
|
|
10
|
+
}
|
|
11
|
+
export declare function httpGet(url: string, headers?: Record<string, string>): Promise<Response>;
|
|
12
|
+
export declare function getText(url: string, headers?: Record<string, string>): Promise<string>;
|
|
13
|
+
export declare function getJson<T = unknown>(url: string, headers?: Record<string, string>): Promise<T>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal fetch wrapper shared by every extractor/provider.
|
|
3
|
+
* Uses Node's global fetch directly — no extra HTTP dependency.
|
|
4
|
+
*/
|
|
5
|
+
const DEFAULT_HEADERS = {
|
|
6
|
+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:144.0) Gecko/20100101 Firefox/144.0",
|
|
7
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
8
|
+
"Accept-Language": "en-US,en;q=0.5",
|
|
9
|
+
};
|
|
10
|
+
export class HttpError extends Error {
|
|
11
|
+
status;
|
|
12
|
+
statusText;
|
|
13
|
+
url;
|
|
14
|
+
constructor(status, statusText, url) {
|
|
15
|
+
super(`HTTP ${status} ${statusText} — ${url}`);
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.statusText = statusText;
|
|
18
|
+
this.url = url;
|
|
19
|
+
this.name = "HttpError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function httpGet(url, headers = {}) {
|
|
23
|
+
return fetch(url, {
|
|
24
|
+
method: "GET",
|
|
25
|
+
headers: { ...DEFAULT_HEADERS, ...headers },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export async function getText(url, headers) {
|
|
29
|
+
const res = await httpGet(url, headers);
|
|
30
|
+
if (!res.ok)
|
|
31
|
+
throw new HttpError(res.status, res.statusText, url);
|
|
32
|
+
return res.text();
|
|
33
|
+
}
|
|
34
|
+
export async function getJson(url, headers) {
|
|
35
|
+
const res = await httpGet(url, headers);
|
|
36
|
+
if (!res.ok)
|
|
37
|
+
throw new HttpError(res.status, res.statusText, url);
|
|
38
|
+
return (await res.json());
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare function encodeMiruro(url: string, referer: string): string;
|
|
2
|
+
export declare function encodeAnikuro(url: string, referer: string): string | undefined;
|
|
3
|
+
export declare function encodeLunar(url: string, referer: string): string;
|
|
4
|
+
export declare function encodeAnimanga(url: string, referer: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Generates anikuro proxy URL for a given video URL and referer,
|
|
7
|
+
* respecting the global proxy configuration.
|
|
8
|
+
*/
|
|
9
|
+
export declare function generateProxiedUrl(url: string, referer?: string): string | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { config } from '../lib/config.js';
|
|
3
|
+
const miruroKey = Buffer.from("a54d389c18527d9fd3e7f0643e27edbe", "hex");
|
|
4
|
+
export function encodeMiruro(url, referer) {
|
|
5
|
+
const encodeParam = (text) => {
|
|
6
|
+
const b = Buffer.from(text, 'utf-8');
|
|
7
|
+
const c = Buffer.alloc(b.length);
|
|
8
|
+
for (let i = 0; i < b.length; i++) {
|
|
9
|
+
c[i] = b[i] ^ miruroKey[i % 16];
|
|
10
|
+
}
|
|
11
|
+
return c.toString('base64url');
|
|
12
|
+
};
|
|
13
|
+
return `https://pro.ultracloud.cc/m3u8/?u=${encodeParam(url)}&r=${encodeParam(referer)}`;
|
|
14
|
+
}
|
|
15
|
+
export function encodeAnikuro(url, referer) {
|
|
16
|
+
if (!config.proxy.enabled || !config.proxy.generateSourceProxies) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
const b64 = Buffer.from(`${url}|${referer}`).toString('base64');
|
|
20
|
+
const ext = url.toLowerCase().includes('.m3u8') ? '.m3u8' : '.mp4';
|
|
21
|
+
return `https://proxy.anikuro.to/${b64}${ext}`;
|
|
22
|
+
}
|
|
23
|
+
function customQuote(str) {
|
|
24
|
+
return encodeURIComponent(str).replace(/%3A/g, ':').replace(/%2F/g, '/');
|
|
25
|
+
}
|
|
26
|
+
export function encodeLunar(url, referer) {
|
|
27
|
+
return `https://cluster.lunaranime.ru/api/proxy/hls/custom?url=${customQuote(url)}&referer=${customQuote(referer)}`;
|
|
28
|
+
}
|
|
29
|
+
export function encodeAnimanga(url, referer) {
|
|
30
|
+
const headers = JSON.stringify({ Referer: referer });
|
|
31
|
+
return `https://upcloud.animanga.fun/proxy?url=${customQuote(url)}&headers=${customQuote(headers)}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generates anikuro proxy URL for a given video URL and referer,
|
|
35
|
+
* respecting the global proxy configuration.
|
|
36
|
+
*/
|
|
37
|
+
export function generateProxiedUrl(url, referer) {
|
|
38
|
+
if (!config.proxy.enabled || !config.proxy.generateSourceProxies) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const ref = referer || url;
|
|
42
|
+
return encodeAnikuro(url, ref);
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { VideoStream } from "../types/types.js";
|
|
2
|
+
/** MegaPlay and VidWish both embed the numeric media id the same way. */
|
|
3
|
+
export declare function parseEmbedMediaId(html: string): string | null;
|
|
4
|
+
export declare function getVideoType(url: string): "hls" | "mp4" | "dash" | "unknown";
|
|
5
|
+
export declare function getAniSkipTimes(malId: number, episodeNumber: number): Promise<{
|
|
6
|
+
op: {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
} | null;
|
|
10
|
+
ed: {
|
|
11
|
+
start: number;
|
|
12
|
+
end: number;
|
|
13
|
+
} | null;
|
|
14
|
+
} | null>;
|
|
15
|
+
export declare function applyAniSkip(stream: VideoStream, malId?: number, episodeNumber?: number): Promise<VideoStream>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { load } from "cheerio";
|
|
2
|
+
/** MegaPlay and VidWish both embed the numeric media id the same way. */
|
|
3
|
+
export function parseEmbedMediaId(html) {
|
|
4
|
+
const $ = load(html);
|
|
5
|
+
const id = $("body div.mg3-player > div.fix-area").attr("data-id")?.trim();
|
|
6
|
+
const fileId = $("title").text().match(/File\s+(\d+)/i)?.[1];
|
|
7
|
+
return id || fileId || null;
|
|
8
|
+
}
|
|
9
|
+
export function getVideoType(url) {
|
|
10
|
+
if (url.includes('.m3u8'))
|
|
11
|
+
return 'hls';
|
|
12
|
+
if (url.includes('.mpd'))
|
|
13
|
+
return 'dash';
|
|
14
|
+
if (url.includes('.mp4'))
|
|
15
|
+
return 'mp4';
|
|
16
|
+
return 'unknown';
|
|
17
|
+
}
|
|
18
|
+
export async function getAniSkipTimes(malId, episodeNumber) {
|
|
19
|
+
try {
|
|
20
|
+
const url = `https://api.aniskip.com/v2/skip-times/${malId}/${episodeNumber}?types=op&types=ed&episodeLength=0`;
|
|
21
|
+
const res = await fetch(url);
|
|
22
|
+
if (!res.ok)
|
|
23
|
+
return null;
|
|
24
|
+
const json = await res.json();
|
|
25
|
+
if (!json.found || !json.results)
|
|
26
|
+
return null;
|
|
27
|
+
let op = null;
|
|
28
|
+
let ed = null;
|
|
29
|
+
for (const item of json.results) {
|
|
30
|
+
const interval = {
|
|
31
|
+
start: Math.round(item.interval.startTime),
|
|
32
|
+
end: Math.round(item.interval.endTime)
|
|
33
|
+
};
|
|
34
|
+
if (item.skipType === 'op')
|
|
35
|
+
op = interval;
|
|
36
|
+
else if (item.skipType === 'ed')
|
|
37
|
+
ed = interval;
|
|
38
|
+
}
|
|
39
|
+
return { op, ed };
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error('AniSkip fetch error:', err);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function applyAniSkip(stream, malId, episodeNumber) {
|
|
47
|
+
if (!malId || !episodeNumber)
|
|
48
|
+
return stream;
|
|
49
|
+
try {
|
|
50
|
+
const skipTimes = await getAniSkipTimes(malId, episodeNumber);
|
|
51
|
+
if (skipTimes) {
|
|
52
|
+
if (skipTimes.op)
|
|
53
|
+
stream.intro = { start: skipTimes.op.start, end: skipTimes.op.end };
|
|
54
|
+
if (skipTimes.ed)
|
|
55
|
+
stream.outro = { start: skipTimes.ed.start, end: skipTimes.ed.end };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
// We swallow the error because failing to get skip times
|
|
60
|
+
// shouldn't stop the user from watching the episode.
|
|
61
|
+
console.debug(`AniSkip fetch failed for MAL:${malId} EP:${episodeNumber}`, e);
|
|
62
|
+
}
|
|
63
|
+
return stream;
|
|
64
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of the classic Dean Edwards "P.A.C.K.E.R." unpacker — decodes the
|
|
3
|
+
* eval(function(p,a,c,k,e,d){...}(...)) obfuscation used by Kwik and StreamWish.
|
|
4
|
+
* Ported line-for-line from the Dart JsUnpack source rather than rewritten,
|
|
5
|
+
* since this is dense offset-based string logic where a "cleaner" rewrite is
|
|
6
|
+
* exactly how subtle bugs get introduced. Round-trip tested against a hand-built
|
|
7
|
+
* packed sample — see the PR notes for the test.
|
|
8
|
+
*/
|
|
9
|
+
export declare function unpackJs(source: string): string;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of the classic Dean Edwards "P.A.C.K.E.R." unpacker — decodes the
|
|
3
|
+
* eval(function(p,a,c,k,e,d){...}(...)) obfuscation used by Kwik and StreamWish.
|
|
4
|
+
* Ported line-for-line from the Dart JsUnpack source rather than rewritten,
|
|
5
|
+
* since this is dense offset-based string logic where a "cleaner" rewrite is
|
|
6
|
+
* exactly how subtle bugs get introduced. Round-trip tested against a hand-built
|
|
7
|
+
* packed sample — see the PR notes for the test.
|
|
8
|
+
*/
|
|
9
|
+
const ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
10
|
+
function toBase10(word) {
|
|
11
|
+
return word.split("").reduce((out, char) => out * ALPHABET.length + ALPHABET.indexOf(char), 0);
|
|
12
|
+
}
|
|
13
|
+
function filterArgs(source) {
|
|
14
|
+
const match = source.match(/}\s*\('(.*)',\s*(.*?),\s*(\d+),\s*'(.*?)'\.split\('\|'\)/);
|
|
15
|
+
if (!match)
|
|
16
|
+
throw new Error("Corrupted p.a.c.k.e.r. data.");
|
|
17
|
+
return {
|
|
18
|
+
payload: match[1],
|
|
19
|
+
symtab: match[4].split("|"),
|
|
20
|
+
// match[2] (radix) and match[3] (word count) are part of the packer format but are
|
|
21
|
+
// never actually read anywhere below — toBase10 always assumes base62, radix and all.
|
|
22
|
+
// Preserved faithfully from the source rather than "fixed", since it's unverified
|
|
23
|
+
// against a live sample and evidently works for whatever this was built against.
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* NOTE: ported as-is from a quirk in the source. This matches a secondary
|
|
28
|
+
* `var _xxx=["..."];` wrapper some packer variants prepend, then strips a number of
|
|
29
|
+
* characters equal to the length of the matched *variable name* (e.g. 7 chars for
|
|
30
|
+
* "_0x1234") off the front of the string. That's very likely not what was intended —
|
|
31
|
+
* you'd expect it to strip the actual declaration using the match's end index, not the
|
|
32
|
+
* name's length. Kept unchanged since I can't verify it against a live packed sample,
|
|
33
|
+
* but if unpacked output ever comes out mysteriously truncated, this is the first place
|
|
34
|
+
* to look.
|
|
35
|
+
*/
|
|
36
|
+
function stripSecondaryStringTable(source) {
|
|
37
|
+
const match = source.match(/var *(_\w+)=\["(.*?)"];/s);
|
|
38
|
+
if (!match)
|
|
39
|
+
return source;
|
|
40
|
+
return source.slice(match[1].length);
|
|
41
|
+
}
|
|
42
|
+
export function unpackJs(source) {
|
|
43
|
+
const { payload, symtab } = filterArgs(source);
|
|
44
|
+
const unescaped = payload.replaceAll("\\\\", "\\").replaceAll("\\'", "'");
|
|
45
|
+
let result = unescaped;
|
|
46
|
+
let correction = 0;
|
|
47
|
+
const wordPattern = /\b\w+\b/g;
|
|
48
|
+
let match;
|
|
49
|
+
while ((match = wordPattern.exec(unescaped)) !== null) {
|
|
50
|
+
const word = match[0];
|
|
51
|
+
const start = match.index;
|
|
52
|
+
const end = start + word.length;
|
|
53
|
+
const index = toBase10(word);
|
|
54
|
+
const lookup = index < symtab.length ? symtab[index] || word : word;
|
|
55
|
+
result = result.slice(0, start + correction) + lookup + result.slice(end + correction);
|
|
56
|
+
correction += lookup.length - word.length;
|
|
57
|
+
}
|
|
58
|
+
return stripSecondaryStringTable(result);
|
|
59
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kaizoku-core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./providers/*": "./dist/providers/*.js",
|
|
10
|
+
"./extractors/*": "./dist/extractors/*.js",
|
|
11
|
+
"./utils/*": "./dist/utils/*.js",
|
|
12
|
+
"./types": "./dist/types/types.js"
|
|
13
|
+
},
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"authors": [
|
|
16
|
+
"https://github.com/Pirate193"
|
|
17
|
+
],
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"cheerio": "^1.0.0-rc.12"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.11.17",
|
|
23
|
+
"@vitest/ui": "^4.1.9",
|
|
24
|
+
"typescript": "^5.3.3",
|
|
25
|
+
"vitest": "^4.1.9"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsc",
|
|
29
|
+
"dev": "tsc -w",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"lint": "eslint src/"
|
|
33
|
+
}
|
|
34
|
+
}
|