offcourse 0.0.2 → 1.0.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/.github/workflows/ci.yml +50 -0
- package/.husky/commit-msg +2 -0
- package/.husky/pre-commit +1 -0
- package/.husky/pre-push +3 -0
- package/.prettierrc +8 -0
- package/.release-it.json +23 -0
- package/ARCHITECTURE.md +233 -0
- package/CHANGELOG.md +78 -0
- package/README.md +255 -20
- package/commitlint.config.js +4 -0
- package/dist/ai/openRouter.d.ts +47 -0
- package/dist/ai/openRouter.d.ts.map +1 -0
- package/dist/ai/openRouter.js +116 -0
- package/dist/ai/openRouter.js.map +1 -0
- package/dist/ai/transcriptPolisher.d.ts +24 -0
- package/dist/ai/transcriptPolisher.d.ts.map +1 -0
- package/dist/ai/transcriptPolisher.js +89 -0
- package/dist/ai/transcriptPolisher.js.map +1 -0
- package/dist/cli/commands/config.d.ts +13 -0
- package/dist/cli/commands/config.d.ts.map +1 -0
- package/dist/cli/commands/config.js +66 -0
- package/dist/cli/commands/config.js.map +1 -0
- package/dist/cli/commands/enrich.d.ts +14 -0
- package/dist/cli/commands/enrich.d.ts.map +1 -0
- package/dist/cli/commands/enrich.js +271 -0
- package/dist/cli/commands/enrich.js.map +1 -0
- package/dist/cli/commands/inspect.d.ts +11 -0
- package/dist/cli/commands/inspect.d.ts.map +1 -0
- package/dist/cli/commands/inspect.js +365 -0
- package/dist/cli/commands/inspect.js.map +1 -0
- package/dist/cli/commands/login.d.ts +12 -0
- package/dist/cli/commands/login.d.ts.map +1 -0
- package/dist/cli/commands/login.js +55 -0
- package/dist/cli/commands/login.js.map +1 -0
- package/dist/cli/commands/status.d.ts +15 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +118 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +16 -0
- package/dist/cli/commands/sync.d.ts.map +1 -0
- package/dist/cli/commands/sync.js +922 -0
- package/dist/cli/commands/sync.js.map +1 -0
- package/dist/cli/commands/syncGhl.d.ts +20 -0
- package/dist/cli/commands/syncGhl.d.ts.map +1 -0
- package/dist/cli/commands/syncGhl.js +483 -0
- package/dist/cli/commands/syncGhl.js.map +1 -0
- package/dist/cli/commands/syncHighLevel.d.ts +24 -0
- package/dist/cli/commands/syncHighLevel.d.ts.map +1 -0
- package/dist/cli/commands/syncHighLevel.js +483 -0
- package/dist/cli/commands/syncHighLevel.js.map +1 -0
- package/dist/cli/commands/syncHighLevel.test.d.ts +2 -0
- package/dist/cli/commands/syncHighLevel.test.d.ts.map +1 -0
- package/dist/cli/commands/syncHighLevel.test.js +102 -0
- package/dist/cli/commands/syncHighLevel.test.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +106 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/configManager.d.ts +31 -0
- package/dist/config/configManager.d.ts.map +1 -0
- package/dist/config/configManager.js +64 -0
- package/dist/config/configManager.js.map +1 -0
- package/dist/config/paths.d.ts +21 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +33 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/paths.test.d.ts +2 -0
- package/dist/config/paths.test.d.ts.map +1 -0
- package/dist/config/paths.test.js +70 -0
- package/dist/config/paths.test.js.map +1 -0
- package/dist/config/schema.d.ts +60 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +50 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/schema.test.d.ts +2 -0
- package/dist/config/schema.test.d.ts.map +1 -0
- package/dist/config/schema.test.js +151 -0
- package/dist/config/schema.test.js.map +1 -0
- package/dist/downloader/hlsDownloader.d.ts +58 -0
- package/dist/downloader/hlsDownloader.d.ts.map +1 -0
- package/dist/downloader/hlsDownloader.js +254 -0
- package/dist/downloader/hlsDownloader.js.map +1 -0
- package/dist/downloader/hlsDownloader.test.d.ts +2 -0
- package/dist/downloader/hlsDownloader.test.d.ts.map +1 -0
- package/dist/downloader/hlsDownloader.test.js +116 -0
- package/dist/downloader/hlsDownloader.test.js.map +1 -0
- package/dist/downloader/hlsValidator.d.ts +35 -0
- package/dist/downloader/hlsValidator.d.ts.map +1 -0
- package/dist/downloader/hlsValidator.js +148 -0
- package/dist/downloader/hlsValidator.js.map +1 -0
- package/dist/downloader/index.d.ts +26 -0
- package/dist/downloader/index.d.ts.map +1 -0
- package/dist/downloader/index.js +52 -0
- package/dist/downloader/index.js.map +1 -0
- package/dist/downloader/loomDownloader.d.ts +56 -0
- package/dist/downloader/loomDownloader.d.ts.map +1 -0
- package/dist/downloader/loomDownloader.js +559 -0
- package/dist/downloader/loomDownloader.js.map +1 -0
- package/dist/downloader/loomDownloader.test.d.ts +2 -0
- package/dist/downloader/loomDownloader.test.d.ts.map +1 -0
- package/dist/downloader/loomDownloader.test.js +36 -0
- package/dist/downloader/loomDownloader.test.js.map +1 -0
- package/dist/downloader/queue.d.ts +56 -0
- package/dist/downloader/queue.d.ts.map +1 -0
- package/dist/downloader/queue.js +88 -0
- package/dist/downloader/queue.js.map +1 -0
- package/dist/downloader/queue.test.d.ts +2 -0
- package/dist/downloader/queue.test.d.ts.map +1 -0
- package/dist/downloader/queue.test.js +158 -0
- package/dist/downloader/queue.test.js.map +1 -0
- package/dist/downloader/videoDownloader.d.ts +32 -0
- package/dist/downloader/videoDownloader.d.ts.map +1 -0
- package/dist/downloader/videoDownloader.js +173 -0
- package/dist/downloader/videoDownloader.js.map +1 -0
- package/dist/downloader/vimeoDownloader.d.ts +52 -0
- package/dist/downloader/vimeoDownloader.d.ts.map +1 -0
- package/dist/downloader/vimeoDownloader.js +565 -0
- package/dist/downloader/vimeoDownloader.js.map +1 -0
- package/dist/downloader/vimeoDownloader.test.d.ts +2 -0
- package/dist/downloader/vimeoDownloader.test.d.ts.map +1 -0
- package/dist/downloader/vimeoDownloader.test.js +51 -0
- package/dist/downloader/vimeoDownloader.test.js.map +1 -0
- package/dist/scraper/auth.d.ts +29 -0
- package/dist/scraper/auth.d.ts.map +1 -0
- package/dist/scraper/auth.js +115 -0
- package/dist/scraper/auth.js.map +1 -0
- package/dist/scraper/extractor.d.ts +49 -0
- package/dist/scraper/extractor.d.ts.map +1 -0
- package/dist/scraper/extractor.js +627 -0
- package/dist/scraper/extractor.js.map +1 -0
- package/dist/scraper/extractor.test.d.ts +2 -0
- package/dist/scraper/extractor.test.d.ts.map +1 -0
- package/dist/scraper/extractor.test.js +65 -0
- package/dist/scraper/extractor.test.js.map +1 -0
- package/dist/scraper/ghl/auth.d.ts +25 -0
- package/dist/scraper/ghl/auth.d.ts.map +1 -0
- package/dist/scraper/ghl/auth.js +187 -0
- package/dist/scraper/ghl/auth.js.map +1 -0
- package/dist/scraper/ghl/extractor.d.ts +96 -0
- package/dist/scraper/ghl/extractor.d.ts.map +1 -0
- package/dist/scraper/ghl/extractor.js +345 -0
- package/dist/scraper/ghl/extractor.js.map +1 -0
- package/dist/scraper/ghl/index.d.ts +4 -0
- package/dist/scraper/ghl/index.d.ts.map +1 -0
- package/dist/scraper/ghl/index.js +4 -0
- package/dist/scraper/ghl/index.js.map +1 -0
- package/dist/scraper/ghl/navigator.d.ts +93 -0
- package/dist/scraper/ghl/navigator.d.ts.map +1 -0
- package/dist/scraper/ghl/navigator.js +447 -0
- package/dist/scraper/ghl/navigator.js.map +1 -0
- package/dist/scraper/highlevel/auth.d.ts +25 -0
- package/dist/scraper/highlevel/auth.d.ts.map +1 -0
- package/dist/scraper/highlevel/auth.js +189 -0
- package/dist/scraper/highlevel/auth.js.map +1 -0
- package/dist/scraper/highlevel/extractor.d.ts +97 -0
- package/dist/scraper/highlevel/extractor.d.ts.map +1 -0
- package/dist/scraper/highlevel/extractor.js +386 -0
- package/dist/scraper/highlevel/extractor.js.map +1 -0
- package/dist/scraper/highlevel/extractor.test.d.ts +2 -0
- package/dist/scraper/highlevel/extractor.test.d.ts.map +1 -0
- package/dist/scraper/highlevel/extractor.test.js +101 -0
- package/dist/scraper/highlevel/extractor.test.js.map +1 -0
- package/dist/scraper/highlevel/index.d.ts +3 -0
- package/dist/scraper/highlevel/index.d.ts.map +1 -0
- package/dist/scraper/highlevel/index.js +3 -0
- package/dist/scraper/highlevel/index.js.map +1 -0
- package/dist/scraper/highlevel/navigator.d.ts +93 -0
- package/dist/scraper/highlevel/navigator.d.ts.map +1 -0
- package/dist/scraper/highlevel/navigator.js +492 -0
- package/dist/scraper/highlevel/navigator.js.map +1 -0
- package/dist/scraper/highlevel/navigator.test.d.ts +2 -0
- package/dist/scraper/highlevel/navigator.test.d.ts.map +1 -0
- package/dist/scraper/highlevel/navigator.test.js +78 -0
- package/dist/scraper/highlevel/navigator.test.js.map +1 -0
- package/dist/scraper/navigator.d.ts +65 -0
- package/dist/scraper/navigator.d.ts.map +1 -0
- package/dist/scraper/navigator.js +300 -0
- package/dist/scraper/navigator.js.map +1 -0
- package/dist/scraper/navigator.test.d.ts +2 -0
- package/dist/scraper/navigator.test.d.ts.map +1 -0
- package/dist/scraper/navigator.test.js +63 -0
- package/dist/scraper/navigator.test.js.map +1 -0
- package/dist/scraper/skoolApi.d.ts +17 -0
- package/dist/scraper/skoolApi.d.ts.map +1 -0
- package/dist/scraper/skoolApi.js +72 -0
- package/dist/scraper/skoolApi.js.map +1 -0
- package/dist/scraper/videoInterceptor.d.ts +19 -0
- package/dist/scraper/videoInterceptor.d.ts.map +1 -0
- package/dist/scraper/videoInterceptor.js +315 -0
- package/dist/scraper/videoInterceptor.js.map +1 -0
- package/dist/shared/auth.d.ts +58 -0
- package/dist/shared/auth.d.ts.map +1 -0
- package/dist/shared/auth.js +211 -0
- package/dist/shared/auth.js.map +1 -0
- package/dist/shared/fs.d.ts +31 -0
- package/dist/shared/fs.d.ts.map +1 -0
- package/dist/shared/fs.js +73 -0
- package/dist/shared/fs.js.map +1 -0
- package/dist/shared/http.d.ts +15 -0
- package/dist/shared/http.d.ts.map +1 -0
- package/dist/shared/http.js +31 -0
- package/dist/shared/http.js.map +1 -0
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +4 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/state/database.d.ts +245 -0
- package/dist/state/database.d.ts.map +1 -0
- package/dist/state/database.js +676 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/database.test.d.ts +2 -0
- package/dist/state/database.test.d.ts.map +1 -0
- package/dist/state/database.test.js +34 -0
- package/dist/state/database.test.js.map +1 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +2 -0
- package/dist/state/index.js.map +1 -0
- package/dist/storage/fileSystem.d.ts +56 -0
- package/dist/storage/fileSystem.d.ts.map +1 -0
- package/dist/storage/fileSystem.js +121 -0
- package/dist/storage/fileSystem.js.map +1 -0
- package/dist/transcription/whisperService.d.ts +27 -0
- package/dist/transcription/whisperService.d.ts.map +1 -0
- package/dist/transcription/whisperService.js +102 -0
- package/dist/transcription/whisperService.js.map +1 -0
- package/eslint.config.js +55 -0
- package/package.json +68 -11
- package/src/__fixtures__/highlevel-post-response.json +68 -0
- package/src/__fixtures__/hls-master-playlist.m3u8 +24 -0
- package/src/cli/commands/__snapshots__/syncHighLevel.test.ts.snap +38 -0
- package/src/cli/commands/config.ts +74 -0
- package/src/cli/commands/inspect.ts +441 -0
- package/src/cli/commands/login.ts +68 -0
- package/src/cli/commands/status.ts +147 -0
- package/src/cli/commands/sync.ts +1235 -0
- package/src/cli/commands/syncHighLevel.test.ts +144 -0
- package/src/cli/commands/syncHighLevel.ts +639 -0
- package/src/cli/index.ts +121 -0
- package/src/config/configManager.ts +75 -0
- package/src/config/paths.test.ts +83 -0
- package/src/config/paths.ts +36 -0
- package/src/config/schema.test.ts +173 -0
- package/src/config/schema.ts +65 -0
- package/src/downloader/hlsDownloader.test.ts +148 -0
- package/src/downloader/hlsDownloader.ts +327 -0
- package/src/downloader/hlsValidator.ts +196 -0
- package/src/downloader/index.ts +122 -0
- package/src/downloader/loomDownloader.test.ts +43 -0
- package/src/downloader/loomDownloader.ts +742 -0
- package/src/downloader/queue.test.ts +199 -0
- package/src/downloader/queue.ts +118 -0
- package/src/downloader/vimeoDownloader.test.ts +62 -0
- package/src/downloader/vimeoDownloader.ts +722 -0
- package/src/scraper/extractor.test.ts +124 -0
- package/src/scraper/extractor.ts +757 -0
- package/src/scraper/highlevel/__snapshots__/extractor.test.ts.snap +41 -0
- package/src/scraper/highlevel/extractor.test.ts +134 -0
- package/src/scraper/highlevel/extractor.ts +537 -0
- package/src/scraper/highlevel/index.ts +2 -0
- package/src/scraper/highlevel/navigator.test.ts +110 -0
- package/src/scraper/highlevel/navigator.ts +668 -0
- package/src/scraper/highlevel/schemas.ts +183 -0
- package/src/scraper/navigator.test.ts +122 -0
- package/src/scraper/navigator.ts +355 -0
- package/src/scraper/schemas.ts +177 -0
- package/src/scraper/videoInterceptor.ts +435 -0
- package/src/shared/auth.test.ts +58 -0
- package/src/shared/auth.ts +251 -0
- package/src/shared/firebase.ts +151 -0
- package/src/shared/fs.ts +80 -0
- package/src/shared/http.ts +34 -0
- package/src/shared/index.ts +6 -0
- package/src/shared/slug.ts +26 -0
- package/src/shared/url.test.ts +122 -0
- package/src/shared/url.ts +57 -0
- package/src/state/database.test.ts +49 -0
- package/src/state/database.ts +919 -0
- package/src/state/index.ts +14 -0
- package/src/storage/fileSystem.test.ts +64 -0
- package/src/storage/fileSystem.ts +175 -0
- package/tsconfig.json +28 -0
- package/vitest.config.ts +29 -0
- package/cli.js +0 -45
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for Skool/Next.js __NEXT_DATA__ responses.
|
|
3
|
+
* These validate only the fields we actually use, ignoring everything else.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Skool Course Child (Module/Lesson)
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
const CourseMetadataSchema = z.looseObject({
|
|
13
|
+
title: z.string().optional(),
|
|
14
|
+
videoLink: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const CourseInfoSchema = z.looseObject({
|
|
18
|
+
id: z.string().optional(),
|
|
19
|
+
name: z.string().optional(), // 8-char hex slug
|
|
20
|
+
metadata: CourseMetadataSchema.optional(),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const CourseChildSchema = z.looseObject({
|
|
24
|
+
course: CourseInfoSchema.optional(),
|
|
25
|
+
hasAccess: z.boolean().optional(),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Skool __NEXT_DATA__ PageProps
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
const SkoolCourseSchema = z.looseObject({
|
|
33
|
+
children: z.array(CourseChildSchema).optional(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const SkoolPagePropsSchema = z.looseObject({
|
|
37
|
+
course: SkoolCourseSchema.optional(),
|
|
38
|
+
selectedModule: z.string().optional(),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const SkoolNextDataSchema = z.looseObject({
|
|
42
|
+
props: z
|
|
43
|
+
.looseObject({
|
|
44
|
+
pageProps: SkoolPagePropsSchema.optional(),
|
|
45
|
+
})
|
|
46
|
+
.optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export type SkoolNextData = z.infer<typeof SkoolNextDataSchema>;
|
|
50
|
+
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Extracted Types (clean types for use in the app)
|
|
53
|
+
// ============================================================================
|
|
54
|
+
|
|
55
|
+
export interface SkoolModule {
|
|
56
|
+
slug: string; // 8-char hex
|
|
57
|
+
title: string;
|
|
58
|
+
hasAccess: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface SkoolLesson {
|
|
62
|
+
id: string;
|
|
63
|
+
hasAccess: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface SkoolVideoInfo {
|
|
67
|
+
url: string;
|
|
68
|
+
type: "loom" | "vimeo" | "youtube" | "wistia" | "unknown";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Helper Functions
|
|
73
|
+
// ============================================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Safely parses __NEXT_DATA__ JSON from a script element.
|
|
77
|
+
* Returns null if parsing fails.
|
|
78
|
+
*/
|
|
79
|
+
export function parseNextData(json: string): SkoolNextData | null {
|
|
80
|
+
try {
|
|
81
|
+
const data: unknown = JSON.parse(json);
|
|
82
|
+
const result = SkoolNextDataSchema.safeParse(data);
|
|
83
|
+
if (result.success) {
|
|
84
|
+
return result.data;
|
|
85
|
+
}
|
|
86
|
+
console.warn("[parseNextData] Validation failed:", z.treeifyError(result.error));
|
|
87
|
+
return null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extracts modules from parsed __NEXT_DATA__.
|
|
95
|
+
*/
|
|
96
|
+
export function extractModulesFromNextData(data: SkoolNextData): SkoolModule[] {
|
|
97
|
+
const children = data.props?.pageProps?.course?.children;
|
|
98
|
+
if (!Array.isArray(children)) return [];
|
|
99
|
+
|
|
100
|
+
const modules: SkoolModule[] = [];
|
|
101
|
+
const seen = new Set<string>();
|
|
102
|
+
|
|
103
|
+
for (const child of children) {
|
|
104
|
+
const course = child.course;
|
|
105
|
+
if (!course?.name) continue;
|
|
106
|
+
|
|
107
|
+
// Skool module slugs are 8-char hex strings
|
|
108
|
+
if (!/^[a-f0-9]{8}$/.test(course.name)) continue;
|
|
109
|
+
|
|
110
|
+
if (seen.has(course.name)) continue;
|
|
111
|
+
seen.add(course.name);
|
|
112
|
+
|
|
113
|
+
modules.push({
|
|
114
|
+
slug: course.name,
|
|
115
|
+
title: course.metadata?.title ?? `Module ${modules.length + 1}`,
|
|
116
|
+
hasAccess: child.hasAccess !== false,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return modules;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extracts lesson access info from parsed __NEXT_DATA__.
|
|
125
|
+
*/
|
|
126
|
+
export function extractLessonAccessFromNextData(data: SkoolNextData): Map<string, boolean> {
|
|
127
|
+
const accessMap = new Map<string, boolean>();
|
|
128
|
+
const children = data.props?.pageProps?.course?.children;
|
|
129
|
+
|
|
130
|
+
if (!Array.isArray(children)) return accessMap;
|
|
131
|
+
|
|
132
|
+
for (const child of children) {
|
|
133
|
+
const id = child.course?.id;
|
|
134
|
+
const hasAccess = child.hasAccess;
|
|
135
|
+
if (id && typeof hasAccess === "boolean") {
|
|
136
|
+
accessMap.set(id, hasAccess);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return accessMap;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Extracts video URL from parsed __NEXT_DATA__ for a specific module.
|
|
145
|
+
*/
|
|
146
|
+
export function extractVideoFromNextData(
|
|
147
|
+
data: SkoolNextData,
|
|
148
|
+
selectedModuleId: string
|
|
149
|
+
): SkoolVideoInfo | null {
|
|
150
|
+
const children = data.props?.pageProps?.course?.children;
|
|
151
|
+
if (!Array.isArray(children)) return null;
|
|
152
|
+
|
|
153
|
+
for (const child of children) {
|
|
154
|
+
if (child.course?.id === selectedModuleId) {
|
|
155
|
+
const videoLink = child.course.metadata?.videoLink;
|
|
156
|
+
if (!videoLink) return null;
|
|
157
|
+
|
|
158
|
+
// Determine video type
|
|
159
|
+
if (videoLink.includes("loom.com")) {
|
|
160
|
+
const embedUrl = videoLink.replace("/share/", "/embed/").split("?")[0];
|
|
161
|
+
return { url: embedUrl ?? videoLink, type: "loom" };
|
|
162
|
+
}
|
|
163
|
+
if (videoLink.includes("vimeo.com")) {
|
|
164
|
+
return { url: videoLink, type: "vimeo" };
|
|
165
|
+
}
|
|
166
|
+
if (videoLink.includes("youtube.com") || videoLink.includes("youtu.be")) {
|
|
167
|
+
return { url: videoLink, type: "youtube" };
|
|
168
|
+
}
|
|
169
|
+
if (videoLink.includes("wistia")) {
|
|
170
|
+
return { url: videoLink, type: "wistia" };
|
|
171
|
+
}
|
|
172
|
+
return { url: videoLink, type: "unknown" };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-based video URL interception - requires Playwright.
|
|
3
|
+
* Excluded from coverage via vitest.config.ts.
|
|
4
|
+
*/
|
|
5
|
+
import type { Page } from "playwright";
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Type definitions for external browser APIs
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
/** Vimeo player configuration embedded in the page */
|
|
12
|
+
interface VimeoPlayerConfig {
|
|
13
|
+
request?: {
|
|
14
|
+
files?: {
|
|
15
|
+
hls?: {
|
|
16
|
+
cdns?: Record<string, { url?: string }>;
|
|
17
|
+
};
|
|
18
|
+
progressive?: { url?: string; height?: number }[];
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Vimeo-related window properties */
|
|
24
|
+
interface VimeoWindow {
|
|
25
|
+
playerConfig?: VimeoPlayerConfig;
|
|
26
|
+
vimeo?: { config?: VimeoPlayerConfig };
|
|
27
|
+
__vimeo_player__?: { config?: VimeoPlayerConfig };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Loom video asset URLs */
|
|
31
|
+
interface LoomAssetUrls {
|
|
32
|
+
hls_url?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Loom video data */
|
|
36
|
+
interface LoomVideo {
|
|
37
|
+
asset_urls?: LoomAssetUrls;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Loom SSR state embedded in the page */
|
|
41
|
+
interface LoomSSRState {
|
|
42
|
+
video?: LoomVideo;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Loom-related window properties */
|
|
46
|
+
interface LoomWindow {
|
|
47
|
+
__LOOM_SSR_STATE__?: LoomSSRState;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Next.js data for Loom pages */
|
|
51
|
+
interface LoomNextData {
|
|
52
|
+
props?: {
|
|
53
|
+
pageProps?: {
|
|
54
|
+
video?: LoomVideo;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Captures Vimeo video URL by extracting it from the running player.
|
|
61
|
+
* The key insight: the video is ALREADY playing in the iframe - we just need to get the URL.
|
|
62
|
+
*/
|
|
63
|
+
export async function captureVimeoConfig(
|
|
64
|
+
page: Page,
|
|
65
|
+
_videoId: string,
|
|
66
|
+
timeoutMs = 20000
|
|
67
|
+
): Promise<{ hlsUrl: string | null; progressiveUrl: string | null; error?: string }> {
|
|
68
|
+
try {
|
|
69
|
+
// Step 1: Make sure we have a Vimeo iframe or video wrapper
|
|
70
|
+
// Skool wraps videos in a VideoPlayerWrapper - click it to ensure video loads
|
|
71
|
+
const videoWrapper = await page.$(
|
|
72
|
+
'[class*="VideoPlayerWrapper"], [class*="video-wrapper"], [class*="VideoPlayer"]'
|
|
73
|
+
);
|
|
74
|
+
if (videoWrapper) {
|
|
75
|
+
await videoWrapper.click().catch(() => {});
|
|
76
|
+
await page.waitForTimeout(1000);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Step 2: Wait for Vimeo iframe to appear
|
|
80
|
+
let vimeoFrame = null;
|
|
81
|
+
const startTime = Date.now();
|
|
82
|
+
|
|
83
|
+
while (!vimeoFrame && Date.now() - startTime < timeoutMs) {
|
|
84
|
+
// Try to find the iframe
|
|
85
|
+
const iframe = await page.$('iframe[src*="vimeo.com"], iframe[src*="player.vimeo"]');
|
|
86
|
+
if (iframe) {
|
|
87
|
+
vimeoFrame = await iframe.contentFrame();
|
|
88
|
+
if (vimeoFrame) break;
|
|
89
|
+
}
|
|
90
|
+
await page.waitForTimeout(500);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!vimeoFrame) {
|
|
94
|
+
return { hlsUrl: null, progressiveUrl: null, error: "Vimeo iframe not found after waiting" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Step 3: Mute the video before playing (we don't want audio!)
|
|
98
|
+
await vimeoFrame
|
|
99
|
+
.evaluate(() => {
|
|
100
|
+
const video = document.querySelector("video");
|
|
101
|
+
if (video) {
|
|
102
|
+
video.muted = true;
|
|
103
|
+
video.volume = 0;
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
.catch(() => {});
|
|
107
|
+
|
|
108
|
+
// Step 4: Click play button in the iframe to start video
|
|
109
|
+
try {
|
|
110
|
+
// Multiple selectors for Vimeo's play button
|
|
111
|
+
await vimeoFrame
|
|
112
|
+
.click(
|
|
113
|
+
'.vp-controls button, .play-icon, [aria-label="Play"], .vp-big-play-button, button',
|
|
114
|
+
{
|
|
115
|
+
timeout: 2000,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
.catch(() => {});
|
|
119
|
+
} catch {
|
|
120
|
+
// Video might auto-play
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Ensure video stays muted
|
|
124
|
+
await vimeoFrame
|
|
125
|
+
.evaluate(() => {
|
|
126
|
+
const video = document.querySelector("video");
|
|
127
|
+
if (video) {
|
|
128
|
+
video.muted = true;
|
|
129
|
+
video.volume = 0;
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
.catch(() => {});
|
|
133
|
+
|
|
134
|
+
// Step 4: Wait for video to actually start playing and get the URL
|
|
135
|
+
let hlsUrl: string | null = null;
|
|
136
|
+
let progressiveUrl: string | null = null;
|
|
137
|
+
|
|
138
|
+
const extractionStart = Date.now();
|
|
139
|
+
while (!hlsUrl && !progressiveUrl && Date.now() - extractionStart < timeoutMs - 5000) {
|
|
140
|
+
const urls = await vimeoFrame.evaluate(() => {
|
|
141
|
+
const result = {
|
|
142
|
+
hlsUrl: null as string | null,
|
|
143
|
+
progressiveUrl: null as string | null,
|
|
144
|
+
debug: [] as string[],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Method 1: Get URL directly from video element
|
|
148
|
+
const video = document.querySelector("video");
|
|
149
|
+
if (video) {
|
|
150
|
+
result.debug.push(`Video element found, src length: ${video.src?.length ?? 0}`);
|
|
151
|
+
|
|
152
|
+
// Check currentSrc (what's actually playing)
|
|
153
|
+
if (video.currentSrc) {
|
|
154
|
+
result.debug.push(`currentSrc: ${video.currentSrc.substring(0, 80)}`);
|
|
155
|
+
if (video.currentSrc.includes(".m3u8")) {
|
|
156
|
+
result.hlsUrl = video.currentSrc;
|
|
157
|
+
} else if (video.currentSrc.includes(".mp4")) {
|
|
158
|
+
result.progressiveUrl = video.currentSrc;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Also check src attribute
|
|
163
|
+
if (!result.hlsUrl && !result.progressiveUrl && video.src) {
|
|
164
|
+
if (video.src.includes(".m3u8")) {
|
|
165
|
+
result.hlsUrl = video.src;
|
|
166
|
+
} else if (video.src.includes(".mp4")) {
|
|
167
|
+
result.progressiveUrl = video.src;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Method 2: Check source elements
|
|
173
|
+
if (!result.hlsUrl && !result.progressiveUrl) {
|
|
174
|
+
const sources = document.querySelectorAll("video source");
|
|
175
|
+
result.debug.push(`Found ${sources.length} source elements`);
|
|
176
|
+
for (const source of Array.from(sources)) {
|
|
177
|
+
const src = (source as HTMLSourceElement).src;
|
|
178
|
+
if (src?.includes(".m3u8")) {
|
|
179
|
+
result.hlsUrl = src;
|
|
180
|
+
break;
|
|
181
|
+
} else if (src?.includes(".mp4") && !result.progressiveUrl) {
|
|
182
|
+
result.progressiveUrl = src;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Method 3: Extract from Vimeo's internal player state
|
|
188
|
+
if (!result.hlsUrl && !result.progressiveUrl) {
|
|
189
|
+
try {
|
|
190
|
+
const win = window as unknown as VimeoWindow;
|
|
191
|
+
|
|
192
|
+
// Try various Vimeo internal variables
|
|
193
|
+
const configPaths = [
|
|
194
|
+
win.playerConfig?.request?.files,
|
|
195
|
+
win.vimeo?.config?.request?.files,
|
|
196
|
+
win.__vimeo_player__?.config?.request?.files,
|
|
197
|
+
];
|
|
198
|
+
|
|
199
|
+
for (const files of configPaths) {
|
|
200
|
+
if (!files) continue;
|
|
201
|
+
|
|
202
|
+
// HLS
|
|
203
|
+
if (files.hls?.cdns) {
|
|
204
|
+
const cdns = files.hls.cdns;
|
|
205
|
+
for (const cdn of Object.keys(cdns)) {
|
|
206
|
+
const cdnEntry = cdns[cdn];
|
|
207
|
+
if (cdnEntry?.url) {
|
|
208
|
+
result.hlsUrl = cdnEntry.url;
|
|
209
|
+
result.debug.push(`Found HLS in playerConfig.${cdn}`);
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Progressive MP4
|
|
216
|
+
if (!result.progressiveUrl && files.progressive && files.progressive.length > 0) {
|
|
217
|
+
const sorted = [...files.progressive].sort(
|
|
218
|
+
(a, b) => (b.height ?? 0) - (a.height ?? 0)
|
|
219
|
+
);
|
|
220
|
+
result.progressiveUrl = sorted[0]?.url ?? null;
|
|
221
|
+
if (result.progressiveUrl) {
|
|
222
|
+
result.debug.push("Found progressive in playerConfig");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (result.hlsUrl || result.progressiveUrl) break;
|
|
227
|
+
}
|
|
228
|
+
} catch (e) {
|
|
229
|
+
result.debug.push(
|
|
230
|
+
`Config extraction error: ${e instanceof Error ? e.message : String(e)}`
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Method 4: Network request URLs might be in DOM attributes
|
|
236
|
+
if (!result.hlsUrl && !result.progressiveUrl) {
|
|
237
|
+
const allElements = document.querySelectorAll("*");
|
|
238
|
+
for (const el of Array.from(allElements)) {
|
|
239
|
+
for (const attr of Array.from(el.attributes)) {
|
|
240
|
+
if (attr.value.includes("vimeocdn.com") && attr.value.includes(".m3u8")) {
|
|
241
|
+
result.hlsUrl = /https:\/\/[^\s"']+\.m3u8[^\s"']*/.exec(attr.value)?.[0] ?? null;
|
|
242
|
+
if (result.hlsUrl) {
|
|
243
|
+
result.debug.push("Found HLS in element attribute");
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (result.hlsUrl) break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return result;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
hlsUrl = urls.hlsUrl;
|
|
256
|
+
progressiveUrl = urls.progressiveUrl;
|
|
257
|
+
|
|
258
|
+
if (!hlsUrl && !progressiveUrl) {
|
|
259
|
+
// Wait and try again
|
|
260
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (hlsUrl || progressiveUrl) {
|
|
265
|
+
return { hlsUrl, progressiveUrl };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
hlsUrl: null,
|
|
270
|
+
progressiveUrl: null,
|
|
271
|
+
error: "Could not extract video URL from Vimeo player",
|
|
272
|
+
};
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return {
|
|
275
|
+
hlsUrl: null,
|
|
276
|
+
progressiveUrl: null,
|
|
277
|
+
error: `Vimeo extraction failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Captures Loom HLS URL by navigating directly to the embed page.
|
|
284
|
+
* This works better than CDP because we can intercept all requests on that page.
|
|
285
|
+
*/
|
|
286
|
+
export async function captureLoomHls(
|
|
287
|
+
page: Page,
|
|
288
|
+
videoId: string,
|
|
289
|
+
timeoutMs = 15000
|
|
290
|
+
): Promise<{ hlsUrl: string | null; error?: string }> {
|
|
291
|
+
let capturedUrl: string | null = null;
|
|
292
|
+
const originalUrl = page.url();
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
// Use CDP to intercept network responses
|
|
296
|
+
const client = await page.context().newCDPSession(page);
|
|
297
|
+
await client.send("Network.enable");
|
|
298
|
+
|
|
299
|
+
// Match HLS playlists from Loom's CDN
|
|
300
|
+
// Prefer master playlist (playlist.m3u8) over media playlists (mediaplaylist-*.m3u8)
|
|
301
|
+
const masterPattern = /luna\.loom\.com.*\/playlist\.m3u8/;
|
|
302
|
+
const anyHlsPattern = /luna\.loom\.com.*\.m3u8/;
|
|
303
|
+
|
|
304
|
+
// Set up listener before navigation
|
|
305
|
+
const responsePromise = new Promise<void>((resolve) => {
|
|
306
|
+
const timeout = setTimeout(() => {
|
|
307
|
+
resolve();
|
|
308
|
+
}, timeoutMs);
|
|
309
|
+
let hasMasterPlaylist = false;
|
|
310
|
+
|
|
311
|
+
client.on("Network.responseReceived", (event) => {
|
|
312
|
+
const url = event.response.url;
|
|
313
|
+
|
|
314
|
+
// Always prefer master playlist
|
|
315
|
+
if (masterPattern.test(url)) {
|
|
316
|
+
capturedUrl = url;
|
|
317
|
+
hasMasterPlaylist = true;
|
|
318
|
+
clearTimeout(timeout);
|
|
319
|
+
resolve();
|
|
320
|
+
} else if (!hasMasterPlaylist && anyHlsPattern.test(url)) {
|
|
321
|
+
// Capture any HLS as fallback, but keep listening for master
|
|
322
|
+
capturedUrl = url;
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Navigate directly to Loom embed with autoplay (muted)
|
|
328
|
+
const embedUrl = `https://www.loom.com/embed/${videoId}?autoplay=1`;
|
|
329
|
+
await page.goto(embedUrl, { waitUntil: "domcontentloaded", timeout: 10000 });
|
|
330
|
+
|
|
331
|
+
// Mute the video immediately
|
|
332
|
+
await page
|
|
333
|
+
.evaluate(() => {
|
|
334
|
+
const video = document.querySelector("video");
|
|
335
|
+
if (video) {
|
|
336
|
+
video.muted = true;
|
|
337
|
+
video.volume = 0;
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
.catch(() => {});
|
|
341
|
+
|
|
342
|
+
// Try to click play button if video doesn't autoplay
|
|
343
|
+
try {
|
|
344
|
+
await page.waitForTimeout(2000);
|
|
345
|
+
const playButton = await page.$(
|
|
346
|
+
'[data-testid="play-button"], .PlayButton, [aria-label="Play"], button[class*="play"]'
|
|
347
|
+
);
|
|
348
|
+
if (playButton) {
|
|
349
|
+
// Mute again before clicking play
|
|
350
|
+
await page
|
|
351
|
+
.evaluate(() => {
|
|
352
|
+
const video = document.querySelector("video");
|
|
353
|
+
if (video) {
|
|
354
|
+
video.muted = true;
|
|
355
|
+
video.volume = 0;
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
.catch(() => {});
|
|
359
|
+
await playButton.click();
|
|
360
|
+
}
|
|
361
|
+
} catch {
|
|
362
|
+
// No play button or click failed
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Ensure video stays muted after play
|
|
366
|
+
await page
|
|
367
|
+
.evaluate(() => {
|
|
368
|
+
const video = document.querySelector("video");
|
|
369
|
+
if (video) {
|
|
370
|
+
video.muted = true;
|
|
371
|
+
video.volume = 0;
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
.catch(() => {});
|
|
375
|
+
|
|
376
|
+
// Wait for HLS to be captured
|
|
377
|
+
await responsePromise;
|
|
378
|
+
|
|
379
|
+
// Also try to extract from page JS if not found via network
|
|
380
|
+
if (!capturedUrl) {
|
|
381
|
+
const jsUrl = await page.evaluate(() => {
|
|
382
|
+
const win = window as unknown as LoomWindow;
|
|
383
|
+
|
|
384
|
+
// Check __LOOM_SSR_STATE__
|
|
385
|
+
if (win.__LOOM_SSR_STATE__?.video?.asset_urls?.hls_url) {
|
|
386
|
+
return win.__LOOM_SSR_STATE__.video.asset_urls.hls_url;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Check for Next.js data
|
|
390
|
+
const nextData = document.getElementById("__NEXT_DATA__");
|
|
391
|
+
if (nextData?.textContent) {
|
|
392
|
+
try {
|
|
393
|
+
const data = JSON.parse(nextData.textContent) as LoomNextData;
|
|
394
|
+
const hlsUrl = data?.props?.pageProps?.video?.asset_urls?.hls_url;
|
|
395
|
+
if (hlsUrl) return hlsUrl;
|
|
396
|
+
|
|
397
|
+
// Try regex match in full data
|
|
398
|
+
const videoData = /hls_url['":\s]+['"]([^'"]+)['"]/.exec(JSON.stringify(data));
|
|
399
|
+
if (videoData?.[1]) return videoData[1];
|
|
400
|
+
} catch {
|
|
401
|
+
/* ignore parse errors */
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Scan scripts for HLS URL
|
|
406
|
+
const scripts = Array.from(document.querySelectorAll("script"));
|
|
407
|
+
for (const script of scripts) {
|
|
408
|
+
const match = /https:\/\/luna\.loom\.com[^"'\s]+\.m3u8[^"'\s]*/.exec(
|
|
409
|
+
script.textContent ?? ""
|
|
410
|
+
);
|
|
411
|
+
if (match) return match[0];
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return null;
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
if (jsUrl) {
|
|
418
|
+
capturedUrl = jsUrl;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
await client.detach();
|
|
423
|
+
} catch {
|
|
424
|
+
// Error during capture
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Navigate back to original page
|
|
428
|
+
try {
|
|
429
|
+
await page.goto(originalUrl, { waitUntil: "domcontentloaded", timeout: 10000 });
|
|
430
|
+
} catch {
|
|
431
|
+
// Failed to navigate back
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return capturedUrl ? { hlsUrl: capturedUrl } : { hlsUrl: null, error: "HLS URL not captured" };
|
|
435
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createLoginChecker, isSkoolLoginPage, isHighLevelLoginPage } from "./auth.js";
|
|
3
|
+
|
|
4
|
+
describe("auth", () => {
|
|
5
|
+
describe("createLoginChecker", () => {
|
|
6
|
+
it("creates checker that matches patterns", () => {
|
|
7
|
+
const checker = createLoginChecker([/\/login/, /\/signin/]);
|
|
8
|
+
|
|
9
|
+
expect(checker("https://example.com/login")).toBe(true);
|
|
10
|
+
expect(checker("https://example.com/signin")).toBe(true);
|
|
11
|
+
expect(checker("https://example.com/dashboard")).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("uses default patterns when none provided", () => {
|
|
15
|
+
const checker = createLoginChecker();
|
|
16
|
+
|
|
17
|
+
expect(checker("https://example.com/login")).toBe(true);
|
|
18
|
+
expect(checker("https://accounts.google.com/auth")).toBe(true);
|
|
19
|
+
expect(checker("https://example.com/dashboard")).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("isSkoolLoginPage", () => {
|
|
24
|
+
it("detects Skool login page", () => {
|
|
25
|
+
expect(isSkoolLoginPage("https://www.skool.com/login")).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("detects Google OAuth redirect", () => {
|
|
29
|
+
expect(isSkoolLoginPage("https://accounts.google.com/o/oauth2/auth?...")).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("returns false for non-login pages", () => {
|
|
33
|
+
expect(isSkoolLoginPage("https://www.skool.com/my-community")).toBe(false);
|
|
34
|
+
expect(isSkoolLoginPage("https://www.skool.com/my-community/classroom")).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("isHighLevelLoginPage", () => {
|
|
39
|
+
it("detects HighLevel SSO page", () => {
|
|
40
|
+
expect(isHighLevelLoginPage("https://sso.clientclub.net/login")).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("detects various login paths", () => {
|
|
44
|
+
expect(isHighLevelLoginPage("https://portal.example.com/login")).toBe(true);
|
|
45
|
+
expect(isHighLevelLoginPage("https://portal.example.com/signin")).toBe(true);
|
|
46
|
+
expect(isHighLevelLoginPage("https://portal.example.com/auth/callback")).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("detects Firebase auth", () => {
|
|
50
|
+
expect(isHighLevelLoginPage("https://example.firebaseapp.com/auth")).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns false for content pages", () => {
|
|
54
|
+
expect(isHighLevelLoginPage("https://portal.example.com/courses")).toBe(false);
|
|
55
|
+
expect(isHighLevelLoginPage("https://portal.example.com/dashboard")).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|