offcourse 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +107 -8
  2. package/dist/cli/commands/config.js.map +1 -1
  3. package/dist/cli/commands/inspect.js +1 -1
  4. package/dist/cli/commands/inspect.js.map +1 -1
  5. package/dist/cli/commands/sync.d.ts +1 -2
  6. package/dist/cli/commands/sync.d.ts.map +1 -1
  7. package/dist/cli/commands/sync.js +17 -15
  8. package/dist/cli/commands/sync.js.map +1 -1
  9. package/dist/cli/commands/syncHighLevel.d.ts +1 -2
  10. package/dist/cli/commands/syncHighLevel.d.ts.map +1 -1
  11. package/dist/cli/commands/syncHighLevel.js +8 -9
  12. package/dist/cli/commands/syncHighLevel.js.map +1 -1
  13. package/dist/cli/commands/syncLearningSuite.d.ts +35 -0
  14. package/dist/cli/commands/syncLearningSuite.d.ts.map +1 -0
  15. package/dist/cli/commands/syncLearningSuite.js +765 -0
  16. package/dist/cli/commands/syncLearningSuite.js.map +1 -0
  17. package/dist/cli/index.js +39 -1
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/config/configManager.d.ts.map +1 -1
  20. package/dist/config/configManager.js +4 -0
  21. package/dist/config/configManager.js.map +1 -1
  22. package/dist/downloader/hlsDownloader.d.ts +10 -4
  23. package/dist/downloader/hlsDownloader.d.ts.map +1 -1
  24. package/dist/downloader/hlsDownloader.js +60 -29
  25. package/dist/downloader/hlsDownloader.js.map +1 -1
  26. package/dist/downloader/hlsValidator.d.ts.map +1 -1
  27. package/dist/downloader/hlsValidator.js +6 -2
  28. package/dist/downloader/hlsValidator.js.map +1 -1
  29. package/dist/downloader/index.d.ts +7 -0
  30. package/dist/downloader/index.d.ts.map +1 -1
  31. package/dist/downloader/index.js +9 -6
  32. package/dist/downloader/index.js.map +1 -1
  33. package/dist/downloader/loomDownloader.d.ts +1 -1
  34. package/dist/downloader/loomDownloader.d.ts.map +1 -1
  35. package/dist/downloader/loomDownloader.js +32 -27
  36. package/dist/downloader/loomDownloader.js.map +1 -1
  37. package/dist/downloader/queue.d.ts +4 -4
  38. package/dist/downloader/queue.d.ts.map +1 -1
  39. package/dist/downloader/queue.js.map +1 -1
  40. package/dist/downloader/vimeoDownloader.d.ts.map +1 -1
  41. package/dist/downloader/vimeoDownloader.js +7 -3
  42. package/dist/downloader/vimeoDownloader.js.map +1 -1
  43. package/dist/scraper/extractor.d.ts +4 -0
  44. package/dist/scraper/extractor.d.ts.map +1 -1
  45. package/dist/scraper/extractor.js +79 -79
  46. package/dist/scraper/extractor.js.map +1 -1
  47. package/dist/scraper/highlevel/extractor.d.ts +11 -19
  48. package/dist/scraper/highlevel/extractor.d.ts.map +1 -1
  49. package/dist/scraper/highlevel/extractor.js +72 -85
  50. package/dist/scraper/highlevel/extractor.js.map +1 -1
  51. package/dist/scraper/highlevel/navigator.d.ts +3 -10
  52. package/dist/scraper/highlevel/navigator.d.ts.map +1 -1
  53. package/dist/scraper/highlevel/navigator.js +140 -127
  54. package/dist/scraper/highlevel/navigator.js.map +1 -1
  55. package/dist/scraper/highlevel/schemas.d.ts +188 -0
  56. package/dist/scraper/highlevel/schemas.d.ts.map +1 -0
  57. package/dist/scraper/highlevel/schemas.js +139 -0
  58. package/dist/scraper/highlevel/schemas.js.map +1 -0
  59. package/dist/scraper/learningsuite/extractor.d.ts +50 -0
  60. package/dist/scraper/learningsuite/extractor.d.ts.map +1 -0
  61. package/dist/scraper/learningsuite/extractor.js +429 -0
  62. package/dist/scraper/learningsuite/extractor.js.map +1 -0
  63. package/dist/scraper/learningsuite/index.d.ts +4 -0
  64. package/dist/scraper/learningsuite/index.d.ts.map +1 -0
  65. package/dist/scraper/{ghl → learningsuite}/index.js +1 -1
  66. package/dist/scraper/learningsuite/index.js.map +1 -0
  67. package/dist/scraper/learningsuite/navigator.d.ts +122 -0
  68. package/dist/scraper/learningsuite/navigator.d.ts.map +1 -0
  69. package/dist/scraper/learningsuite/navigator.js +736 -0
  70. package/dist/scraper/learningsuite/navigator.js.map +1 -0
  71. package/dist/scraper/learningsuite/schemas.d.ts +270 -0
  72. package/dist/scraper/learningsuite/schemas.d.ts.map +1 -0
  73. package/dist/scraper/learningsuite/schemas.js +147 -0
  74. package/dist/scraper/learningsuite/schemas.js.map +1 -0
  75. package/dist/scraper/navigator.d.ts +14 -11
  76. package/dist/scraper/navigator.d.ts.map +1 -1
  77. package/dist/scraper/navigator.js +61 -104
  78. package/dist/scraper/navigator.js.map +1 -1
  79. package/dist/scraper/schemas.d.ts +57 -0
  80. package/dist/scraper/schemas.d.ts.map +1 -0
  81. package/dist/scraper/schemas.js +135 -0
  82. package/dist/scraper/schemas.js.map +1 -0
  83. package/dist/scraper/videoInterceptor.d.ts +4 -0
  84. package/dist/scraper/videoInterceptor.d.ts.map +1 -1
  85. package/dist/scraper/videoInterceptor.js +66 -51
  86. package/dist/scraper/videoInterceptor.js.map +1 -1
  87. package/dist/shared/auth.d.ts +9 -9
  88. package/dist/shared/auth.d.ts.map +1 -1
  89. package/dist/shared/auth.js +24 -38
  90. package/dist/shared/auth.js.map +1 -1
  91. package/dist/shared/firebase.d.ts +60 -0
  92. package/dist/shared/firebase.d.ts.map +1 -0
  93. package/dist/shared/firebase.js +102 -0
  94. package/dist/shared/firebase.js.map +1 -0
  95. package/dist/shared/fs.d.ts.map +1 -1
  96. package/dist/shared/fs.js +4 -0
  97. package/dist/shared/fs.js.map +1 -1
  98. package/dist/shared/index.d.ts +3 -0
  99. package/dist/shared/index.d.ts.map +1 -1
  100. package/dist/shared/index.js +3 -0
  101. package/dist/shared/index.js.map +1 -1
  102. package/dist/shared/slug.d.ts +11 -0
  103. package/dist/shared/slug.d.ts.map +1 -0
  104. package/{src/shared/slug.ts → dist/shared/slug.js} +10 -11
  105. package/dist/shared/slug.js.map +1 -0
  106. package/dist/shared/url.d.ts +43 -0
  107. package/dist/shared/url.d.ts.map +1 -0
  108. package/{src/shared/url.ts → dist/shared/url.js} +12 -15
  109. package/dist/shared/url.js.map +1 -0
  110. package/dist/state/database.d.ts +1 -0
  111. package/dist/state/database.d.ts.map +1 -1
  112. package/dist/state/database.js +3 -0
  113. package/dist/state/database.js.map +1 -1
  114. package/dist/storage/fileSystem.d.ts +17 -17
  115. package/dist/storage/fileSystem.d.ts.map +1 -1
  116. package/dist/storage/fileSystem.js +39 -31
  117. package/dist/storage/fileSystem.js.map +1 -1
  118. package/package.json +5 -2
  119. package/.github/workflows/ci.yml +0 -50
  120. package/.husky/commit-msg +0 -2
  121. package/.husky/pre-commit +0 -1
  122. package/.husky/pre-push +0 -3
  123. package/.prettierrc +0 -8
  124. package/.release-it.json +0 -23
  125. package/ARCHITECTURE.md +0 -233
  126. package/CHANGELOG.md +0 -78
  127. package/commitlint.config.js +0 -4
  128. package/dist/ai/openRouter.d.ts +0 -47
  129. package/dist/ai/openRouter.d.ts.map +0 -1
  130. package/dist/ai/openRouter.js +0 -116
  131. package/dist/ai/openRouter.js.map +0 -1
  132. package/dist/ai/transcriptPolisher.d.ts +0 -24
  133. package/dist/ai/transcriptPolisher.d.ts.map +0 -1
  134. package/dist/ai/transcriptPolisher.js +0 -89
  135. package/dist/ai/transcriptPolisher.js.map +0 -1
  136. package/dist/cli/commands/enrich.d.ts +0 -14
  137. package/dist/cli/commands/enrich.d.ts.map +0 -1
  138. package/dist/cli/commands/enrich.js +0 -271
  139. package/dist/cli/commands/enrich.js.map +0 -1
  140. package/dist/cli/commands/syncGhl.d.ts +0 -20
  141. package/dist/cli/commands/syncGhl.d.ts.map +0 -1
  142. package/dist/cli/commands/syncGhl.js +0 -483
  143. package/dist/cli/commands/syncGhl.js.map +0 -1
  144. package/dist/cli/commands/syncHighLevel.test.d.ts +0 -2
  145. package/dist/cli/commands/syncHighLevel.test.d.ts.map +0 -1
  146. package/dist/cli/commands/syncHighLevel.test.js +0 -102
  147. package/dist/cli/commands/syncHighLevel.test.js.map +0 -1
  148. package/dist/config/paths.test.d.ts +0 -2
  149. package/dist/config/paths.test.d.ts.map +0 -1
  150. package/dist/config/paths.test.js +0 -70
  151. package/dist/config/paths.test.js.map +0 -1
  152. package/dist/config/schema.test.d.ts +0 -2
  153. package/dist/config/schema.test.d.ts.map +0 -1
  154. package/dist/config/schema.test.js +0 -151
  155. package/dist/config/schema.test.js.map +0 -1
  156. package/dist/downloader/hlsDownloader.test.d.ts +0 -2
  157. package/dist/downloader/hlsDownloader.test.d.ts.map +0 -1
  158. package/dist/downloader/hlsDownloader.test.js +0 -116
  159. package/dist/downloader/hlsDownloader.test.js.map +0 -1
  160. package/dist/downloader/loomDownloader.test.d.ts +0 -2
  161. package/dist/downloader/loomDownloader.test.d.ts.map +0 -1
  162. package/dist/downloader/loomDownloader.test.js +0 -36
  163. package/dist/downloader/loomDownloader.test.js.map +0 -1
  164. package/dist/downloader/queue.test.d.ts +0 -2
  165. package/dist/downloader/queue.test.d.ts.map +0 -1
  166. package/dist/downloader/queue.test.js +0 -158
  167. package/dist/downloader/queue.test.js.map +0 -1
  168. package/dist/downloader/videoDownloader.d.ts +0 -32
  169. package/dist/downloader/videoDownloader.d.ts.map +0 -1
  170. package/dist/downloader/videoDownloader.js +0 -173
  171. package/dist/downloader/videoDownloader.js.map +0 -1
  172. package/dist/downloader/vimeoDownloader.test.d.ts +0 -2
  173. package/dist/downloader/vimeoDownloader.test.d.ts.map +0 -1
  174. package/dist/downloader/vimeoDownloader.test.js +0 -51
  175. package/dist/downloader/vimeoDownloader.test.js.map +0 -1
  176. package/dist/scraper/auth.d.ts +0 -29
  177. package/dist/scraper/auth.d.ts.map +0 -1
  178. package/dist/scraper/auth.js +0 -115
  179. package/dist/scraper/auth.js.map +0 -1
  180. package/dist/scraper/extractor.test.d.ts +0 -2
  181. package/dist/scraper/extractor.test.d.ts.map +0 -1
  182. package/dist/scraper/extractor.test.js +0 -65
  183. package/dist/scraper/extractor.test.js.map +0 -1
  184. package/dist/scraper/ghl/auth.d.ts +0 -25
  185. package/dist/scraper/ghl/auth.d.ts.map +0 -1
  186. package/dist/scraper/ghl/auth.js +0 -187
  187. package/dist/scraper/ghl/auth.js.map +0 -1
  188. package/dist/scraper/ghl/extractor.d.ts +0 -96
  189. package/dist/scraper/ghl/extractor.d.ts.map +0 -1
  190. package/dist/scraper/ghl/extractor.js +0 -345
  191. package/dist/scraper/ghl/extractor.js.map +0 -1
  192. package/dist/scraper/ghl/index.d.ts +0 -4
  193. package/dist/scraper/ghl/index.d.ts.map +0 -1
  194. package/dist/scraper/ghl/index.js.map +0 -1
  195. package/dist/scraper/ghl/navigator.d.ts +0 -93
  196. package/dist/scraper/ghl/navigator.d.ts.map +0 -1
  197. package/dist/scraper/ghl/navigator.js +0 -447
  198. package/dist/scraper/ghl/navigator.js.map +0 -1
  199. package/dist/scraper/highlevel/auth.d.ts +0 -25
  200. package/dist/scraper/highlevel/auth.d.ts.map +0 -1
  201. package/dist/scraper/highlevel/auth.js +0 -189
  202. package/dist/scraper/highlevel/auth.js.map +0 -1
  203. package/dist/scraper/highlevel/extractor.test.d.ts +0 -2
  204. package/dist/scraper/highlevel/extractor.test.d.ts.map +0 -1
  205. package/dist/scraper/highlevel/extractor.test.js +0 -101
  206. package/dist/scraper/highlevel/extractor.test.js.map +0 -1
  207. package/dist/scraper/highlevel/navigator.test.d.ts +0 -2
  208. package/dist/scraper/highlevel/navigator.test.d.ts.map +0 -1
  209. package/dist/scraper/highlevel/navigator.test.js +0 -78
  210. package/dist/scraper/highlevel/navigator.test.js.map +0 -1
  211. package/dist/scraper/navigator.test.d.ts +0 -2
  212. package/dist/scraper/navigator.test.d.ts.map +0 -1
  213. package/dist/scraper/navigator.test.js +0 -63
  214. package/dist/scraper/navigator.test.js.map +0 -1
  215. package/dist/scraper/skoolApi.d.ts +0 -17
  216. package/dist/scraper/skoolApi.d.ts.map +0 -1
  217. package/dist/scraper/skoolApi.js +0 -72
  218. package/dist/scraper/skoolApi.js.map +0 -1
  219. package/dist/state/database.test.d.ts +0 -2
  220. package/dist/state/database.test.d.ts.map +0 -1
  221. package/dist/state/database.test.js +0 -34
  222. package/dist/state/database.test.js.map +0 -1
  223. package/dist/transcription/whisperService.d.ts +0 -27
  224. package/dist/transcription/whisperService.d.ts.map +0 -1
  225. package/dist/transcription/whisperService.js +0 -102
  226. package/dist/transcription/whisperService.js.map +0 -1
  227. package/eslint.config.js +0 -55
  228. package/src/__fixtures__/highlevel-post-response.json +0 -68
  229. package/src/__fixtures__/hls-master-playlist.m3u8 +0 -24
  230. package/src/cli/commands/__snapshots__/syncHighLevel.test.ts.snap +0 -38
  231. package/src/cli/commands/config.ts +0 -74
  232. package/src/cli/commands/inspect.ts +0 -441
  233. package/src/cli/commands/login.ts +0 -68
  234. package/src/cli/commands/status.ts +0 -147
  235. package/src/cli/commands/sync.ts +0 -1235
  236. package/src/cli/commands/syncHighLevel.test.ts +0 -144
  237. package/src/cli/commands/syncHighLevel.ts +0 -639
  238. package/src/cli/index.ts +0 -121
  239. package/src/config/configManager.ts +0 -75
  240. package/src/config/paths.test.ts +0 -83
  241. package/src/config/paths.ts +0 -36
  242. package/src/config/schema.test.ts +0 -173
  243. package/src/config/schema.ts +0 -65
  244. package/src/downloader/hlsDownloader.test.ts +0 -148
  245. package/src/downloader/hlsDownloader.ts +0 -327
  246. package/src/downloader/hlsValidator.ts +0 -196
  247. package/src/downloader/index.ts +0 -122
  248. package/src/downloader/loomDownloader.test.ts +0 -43
  249. package/src/downloader/loomDownloader.ts +0 -742
  250. package/src/downloader/queue.test.ts +0 -199
  251. package/src/downloader/queue.ts +0 -118
  252. package/src/downloader/vimeoDownloader.test.ts +0 -62
  253. package/src/downloader/vimeoDownloader.ts +0 -722
  254. package/src/scraper/extractor.test.ts +0 -124
  255. package/src/scraper/extractor.ts +0 -757
  256. package/src/scraper/highlevel/__snapshots__/extractor.test.ts.snap +0 -41
  257. package/src/scraper/highlevel/extractor.test.ts +0 -134
  258. package/src/scraper/highlevel/extractor.ts +0 -537
  259. package/src/scraper/highlevel/index.ts +0 -2
  260. package/src/scraper/highlevel/navigator.test.ts +0 -110
  261. package/src/scraper/highlevel/navigator.ts +0 -668
  262. package/src/scraper/highlevel/schemas.ts +0 -183
  263. package/src/scraper/navigator.test.ts +0 -122
  264. package/src/scraper/navigator.ts +0 -355
  265. package/src/scraper/schemas.ts +0 -177
  266. package/src/scraper/videoInterceptor.ts +0 -435
  267. package/src/shared/auth.test.ts +0 -58
  268. package/src/shared/auth.ts +0 -251
  269. package/src/shared/firebase.ts +0 -151
  270. package/src/shared/fs.ts +0 -80
  271. package/src/shared/http.ts +0 -34
  272. package/src/shared/index.ts +0 -6
  273. package/src/shared/url.test.ts +0 -122
  274. package/src/state/database.test.ts +0 -49
  275. package/src/state/database.ts +0 -919
  276. package/src/state/index.ts +0 -14
  277. package/src/storage/fileSystem.test.ts +0 -64
  278. package/src/storage/fileSystem.ts +0 -175
  279. package/tsconfig.json +0 -28
  280. package/vitest.config.ts +0 -29
@@ -1,251 +0,0 @@
1
- import type { Browser, BrowserContext, Page } from "playwright";
2
- import { chromium } from "playwright";
3
- import { getSessionPath, SESSIONS_DIR } from "../config/paths.js";
4
- import { ensureDir, outputJson, pathExists, readJson, removeFile } from "./fs.js";
5
-
6
- export interface AuthSession {
7
- context: BrowserContext;
8
- page: Page;
9
- }
10
-
11
- export interface AuthConfig {
12
- /** Domain to store session under */
13
- domain: string;
14
- /** URL to navigate to for login */
15
- loginUrl: string;
16
- /** Function to check if current URL is a login page */
17
- isLoginPage: (url: string) => boolean;
18
- /** Optional: Function to verify session is valid after navigation */
19
- verifySession?: (page: Page) => Promise<boolean>;
20
- /** Login timeout in ms (default: 5 minutes) */
21
- loginTimeout?: number;
22
- }
23
-
24
- /**
25
- * Default login page detection patterns.
26
- */
27
- const DEFAULT_LOGIN_PATTERNS = [
28
- /\/login/,
29
- /\/signin/,
30
- /\/auth/,
31
- /accounts\.google\.com/,
32
- /firebaseapp\.com/,
33
- /sso\./,
34
- ];
35
-
36
- /**
37
- * Creates a login page checker from patterns.
38
- */
39
- export function createLoginChecker(
40
- patterns: RegExp[] = DEFAULT_LOGIN_PATTERNS
41
- ): (url: string) => boolean {
42
- return (url: string) => patterns.some((p) => p.test(url));
43
- }
44
-
45
- /**
46
- * Skool-specific login page checker.
47
- */
48
- export const isSkoolLoginPage = createLoginChecker([/\/login/, /accounts\.google\.com/]);
49
-
50
- /**
51
- * HighLevel-specific login page checker.
52
- */
53
- export const isHighLevelLoginPage = createLoginChecker([
54
- /sso\.clientclub\.net/,
55
- /\/login/,
56
- /\/signin/,
57
- /\/auth/,
58
- /accounts\.google\.com/,
59
- /firebaseapp\.com/,
60
- ]);
61
-
62
- // ============================================
63
- // Browser automation - not unit testable
64
- // ============================================
65
- /* v8 ignore start */
66
-
67
- /**
68
- * Checks if a valid session exists for the given domain.
69
- */
70
- export async function hasValidSession(domain: string): Promise<boolean> {
71
- const sessionPath = getSessionPath(domain);
72
- return pathExists(sessionPath);
73
- }
74
-
75
- /**
76
- * Loads an existing session from disk.
77
- */
78
- async function loadSession(browser: Browser, domain: string): Promise<BrowserContext> {
79
- const sessionPath = getSessionPath(domain);
80
- // Playwright's storageState type is complex, we load it as-is from JSON
81
- const storageState = await readJson(sessionPath);
82
- if (!storageState) {
83
- throw new Error("Session file not found or invalid");
84
- }
85
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
86
- return browser.newContext({ storageState: storageState as any });
87
- }
88
-
89
- /**
90
- * Saves the current session to disk.
91
- */
92
- async function saveSession(context: BrowserContext, domain: string): Promise<void> {
93
- const sessionPath = getSessionPath(domain);
94
- const storageState = await context.storageState();
95
- await outputJson(sessionPath, storageState);
96
- }
97
-
98
- /**
99
- * Performs interactive login by opening a browser window.
100
- * The user logs in manually, and we capture the session.
101
- */
102
- export async function performInteractiveLogin(config: AuthConfig): Promise<AuthSession> {
103
- await ensureDir(SESSIONS_DIR);
104
-
105
- const browser = await chromium.launch({
106
- headless: false, // Must be visible for user interaction
107
- });
108
-
109
- const context = await browser.newContext({
110
- viewport: { width: 1280, height: 800 },
111
- });
112
-
113
- const page = await context.newPage();
114
- await page.goto(config.loginUrl);
115
-
116
- console.log("\n🔐 Browser opened. Please log in manually.");
117
- console.log(" The window will close automatically after successful login.\n");
118
-
119
- const timeout = config.loginTimeout ?? 300000; // 5 minutes default
120
- const startTime = Date.now();
121
- let loggedIn = false;
122
-
123
- while (!loggedIn && Date.now() - startTime < timeout) {
124
- await page.waitForTimeout(1000);
125
-
126
- const currentUrl = page.url();
127
-
128
- // Check if we're no longer on a login page
129
- if (!config.isLoginPage(currentUrl)) {
130
- // Optionally verify with custom function
131
- if (config.verifySession) {
132
- loggedIn = await config.verifySession(page);
133
- } else {
134
- loggedIn = true;
135
- }
136
- }
137
- }
138
-
139
- if (!loggedIn) {
140
- await browser.close();
141
- throw new Error(`Login timed out after ${timeout / 1000} seconds`);
142
- }
143
-
144
- // Give the page a moment to fully load after login
145
- await page.waitForLoadState("networkidle").catch(() => {});
146
- await page.waitForTimeout(1000);
147
-
148
- // Save the session
149
- await saveSession(context, config.domain);
150
-
151
- console.log("✅ Login successful! Session saved.\n");
152
-
153
- return { context, page };
154
- }
155
-
156
- /**
157
- * Gets an authenticated session, either from cache or via interactive login.
158
- */
159
- export async function getAuthenticatedSession(
160
- config: AuthConfig,
161
- options: { forceLogin?: boolean; headless?: boolean } = {}
162
- ): Promise<{ browser: Browser; session: AuthSession }> {
163
- const useHeadless = options.headless !== false;
164
-
165
- const browser = await chromium.launch({
166
- headless: useHeadless,
167
- });
168
-
169
- // Try to use existing session
170
- if (!options.forceLogin && (await hasValidSession(config.domain))) {
171
- try {
172
- const context = await loadSession(browser, config.domain);
173
- const page = await context.newPage();
174
-
175
- // Navigate to verify session
176
- await page.goto(config.loginUrl);
177
- await page.waitForLoadState("domcontentloaded");
178
- await page.waitForTimeout(2000);
179
-
180
- const currentUrl = page.url();
181
-
182
- // Check if we got redirected to login
183
- if (config.isLoginPage(currentUrl)) {
184
- console.log("⚠️ Session expired, need to re-login...");
185
- await context.close();
186
- await browser.close();
187
- } else {
188
- // Optionally verify session
189
- if (config.verifySession) {
190
- const isValid = await config.verifySession(page);
191
- if (!isValid) {
192
- console.log("⚠️ Session invalid, need to re-login...");
193
- await context.close();
194
- await browser.close();
195
- } else {
196
- console.log("✅ Using cached session");
197
- return { browser, session: { context, page } };
198
- }
199
- } else {
200
- console.log("✅ Using cached session");
201
- return { browser, session: { context, page } };
202
- }
203
- }
204
- } catch (error) {
205
- console.log("⚠️ Failed to load session, need to re-login...", error);
206
- await browser.close();
207
- }
208
- } else {
209
- await browser.close();
210
- }
211
-
212
- // Need fresh login - always visible for interactive login
213
- const session = await performInteractiveLogin(config);
214
-
215
- // Get the browser from the session context
216
- const sessionBrowser = session.context.browser();
217
- if (!sessionBrowser) {
218
- throw new Error("Failed to get browser from session");
219
- }
220
-
221
- // After login, reopen with headless browser if needed
222
- if (useHeadless) {
223
- const newBrowser = await chromium.launch({ headless: true });
224
- const context = await loadSession(newBrowser, config.domain);
225
- const page = await context.newPage();
226
-
227
- // Close the interactive session
228
- await sessionBrowser.close();
229
-
230
- return { browser: newBrowser, session: { context, page } };
231
- }
232
-
233
- return { browser: sessionBrowser, session };
234
- }
235
-
236
- /**
237
- * Clears the session for a domain.
238
- */
239
- export async function clearSession(domain: string): Promise<boolean> {
240
- const sessionPath = getSessionPath(domain);
241
- return removeFile(sessionPath);
242
- }
243
-
244
- /**
245
- * Checks if the page has a valid Firebase auth token.
246
- * Used by HighLevel/GoHighLevel portals.
247
- */
248
- // Re-export Firebase auth utilities (used by multiple platforms)
249
- export { hasValidFirebaseToken } from "./firebase.js";
250
-
251
- /* v8 ignore stop */
@@ -1,151 +0,0 @@
1
- /**
2
- * Firebase Authentication utilities.
3
- *
4
- * Firebase Auth is used by many platforms (HighLevel, etc.) for user authentication.
5
- * This module provides shared types, schemas, and utilities for Firebase Auth tokens
6
- * stored in localStorage.
7
- */
8
-
9
- import { z } from "zod";
10
- import type { Page } from "playwright";
11
-
12
- // ============================================================================
13
- // Schemas & Types
14
- // ============================================================================
15
-
16
- /**
17
- * Zod schema for validated Firebase auth token from localStorage.
18
- * Use this after parsing to ensure the token has all required fields.
19
- */
20
- export const FirebaseAuthTokenSchema = z.object({
21
- stsTokenManager: z.object({
22
- accessToken: z.string(),
23
- expirationTime: z.number().optional(),
24
- refreshToken: z.string().optional(),
25
- }),
26
- });
27
-
28
- export type FirebaseAuthToken = z.infer<typeof FirebaseAuthTokenSchema>;
29
-
30
- /**
31
- * Raw Firebase auth data structure from localStorage (before validation).
32
- * Use this type when parsing JSON from localStorage.
33
- */
34
- export interface FirebaseAuthRaw {
35
- stsTokenManager?: {
36
- accessToken?: string;
37
- expirationTime?: number;
38
- refreshToken?: string;
39
- };
40
- }
41
-
42
- // ============================================================================
43
- // localStorage Key Detection
44
- // ============================================================================
45
-
46
- /**
47
- * The localStorage key pattern for Firebase auth tokens.
48
- * Firebase stores auth tokens with keys like "firebase:authUser:API_KEY:[DEFAULT]"
49
- */
50
- export const FIREBASE_AUTH_KEY_PATTERN = "firebase:authUser";
51
-
52
- /**
53
- * Finds the Firebase auth token key in localStorage.
54
- * Returns null if no Firebase auth token is found.
55
- */
56
- export function findFirebaseAuthKey(storage: Storage): string | null {
57
- return Object.keys(storage).find((k) => k.includes(FIREBASE_AUTH_KEY_PATTERN)) ?? null;
58
- }
59
-
60
- // ============================================================================
61
- // Token Utilities
62
- // ============================================================================
63
-
64
- /**
65
- * Checks if a Firebase auth token is expired.
66
- */
67
- export function isTokenExpired(token: FirebaseAuthRaw): boolean {
68
- const expirationTime = token.stsTokenManager?.expirationTime;
69
- if (!expirationTime) {
70
- // No expiration time means we can't determine if expired
71
- // Assume valid if we have an access token
72
- return !token.stsTokenManager?.accessToken;
73
- }
74
- return Date.now() >= expirationTime;
75
- }
76
-
77
- /**
78
- * Extracts the access token from Firebase auth data.
79
- */
80
- export function getAccessToken(token: FirebaseAuthRaw): string | undefined {
81
- return token.stsTokenManager?.accessToken;
82
- }
83
-
84
- // ============================================================================
85
- // Page Utilities (Playwright)
86
- // ============================================================================
87
-
88
- /* v8 ignore start */
89
-
90
- /**
91
- * Checks if the page has a valid (non-expired) Firebase auth token in localStorage.
92
- */
93
- export async function hasValidFirebaseToken(page: Page): Promise<boolean> {
94
- try {
95
- return await page.evaluate(
96
- ({ keyPattern }) => {
97
- const tokenKey = Object.keys(localStorage).find((k) => k.includes(keyPattern));
98
- if (!tokenKey) return false;
99
-
100
- interface FirebaseAuthData {
101
- stsTokenManager?: {
102
- accessToken?: string;
103
- expirationTime?: number;
104
- };
105
- }
106
-
107
- const tokenData = JSON.parse(localStorage.getItem(tokenKey) ?? "{}") as FirebaseAuthData;
108
- const expirationTime = tokenData?.stsTokenManager?.expirationTime;
109
-
110
- if (expirationTime) {
111
- return Date.now() < expirationTime;
112
- }
113
-
114
- return !!tokenData?.stsTokenManager?.accessToken;
115
- },
116
- { keyPattern: FIREBASE_AUTH_KEY_PATTERN }
117
- );
118
- } catch {
119
- return false;
120
- }
121
- }
122
-
123
- /**
124
- * Extracts the Firebase auth token from the page's localStorage.
125
- * Returns the raw token data or null if not found.
126
- */
127
- export async function extractFirebaseAuthFromPage(page: Page): Promise<FirebaseAuthRaw | null> {
128
- return page.evaluate(
129
- ({ keyPattern }): FirebaseAuthRaw | null => {
130
- const tokenKey = Object.keys(localStorage).find((k) => k.includes(keyPattern));
131
- if (!tokenKey) return null;
132
-
133
- interface FirebaseAuthData {
134
- stsTokenManager?: {
135
- accessToken?: string;
136
- expirationTime?: number;
137
- refreshToken?: string;
138
- };
139
- }
140
-
141
- try {
142
- return JSON.parse(localStorage.getItem(tokenKey) ?? "{}") as FirebaseAuthData;
143
- } catch {
144
- return null;
145
- }
146
- },
147
- { keyPattern: FIREBASE_AUTH_KEY_PATTERN }
148
- );
149
- }
150
-
151
- /* v8 ignore stop */
package/src/shared/fs.ts DELETED
@@ -1,80 +0,0 @@
1
- /**
2
- * Thin wrappers around fs/promises for common operations.
3
- * Excluded from coverage - testing would just test Node.js itself.
4
- */
5
- import { mkdir, readFile, writeFile, unlink, access, stat } from "node:fs/promises";
6
- import { dirname } from "node:path";
7
-
8
- /**
9
- * Check if a file or directory exists.
10
- */
11
- export async function pathExists(path: string): Promise<boolean> {
12
- try {
13
- await access(path);
14
- return true;
15
- } catch {
16
- return false;
17
- }
18
- }
19
-
20
- /**
21
- * Ensure a directory exists, creating it recursively if needed.
22
- */
23
- export async function ensureDir(dir: string): Promise<void> {
24
- await mkdir(dir, { recursive: true });
25
- }
26
-
27
- /**
28
- * Write a file, creating parent directories if needed.
29
- */
30
- export async function outputFile(path: string, data: string): Promise<void> {
31
- await ensureDir(dirname(path));
32
- await writeFile(path, data, "utf-8");
33
- }
34
-
35
- /**
36
- * Write JSON to a file, creating parent directories if needed.
37
- */
38
- export async function outputJson(path: string, data: unknown): Promise<void> {
39
- await outputFile(path, JSON.stringify(data, null, 2));
40
- }
41
-
42
- /**
43
- * Read and parse a JSON file.
44
- * Returns null if file doesn't exist or can't be parsed.
45
- */
46
- export async function readJson<T = unknown>(path: string): Promise<T | null> {
47
- try {
48
- const content = await readFile(path, "utf-8");
49
- return JSON.parse(content) as T;
50
- } catch {
51
- return null;
52
- }
53
- }
54
-
55
- /**
56
- * Remove a file if it exists.
57
- */
58
- export async function removeFile(path: string): Promise<boolean> {
59
- try {
60
- await unlink(path);
61
- return true;
62
- } catch {
63
- return false;
64
- }
65
- }
66
-
67
- /**
68
- * Get file size in bytes, or null if file doesn't exist.
69
- */
70
- export async function getFileSize(path: string): Promise<number | null> {
71
- try {
72
- const stats = await stat(path);
73
- return stats.size;
74
- } catch {
75
- return null;
76
- }
77
- }
78
-
79
- // Re-export commonly used fs/promises functions
80
- export { readFile, writeFile, mkdir, unlink, stat } from "node:fs/promises";
@@ -1,34 +0,0 @@
1
- import ky from "ky";
2
-
3
- /**
4
- * Default User-Agent for HTTP requests.
5
- * Mimics a standard Chrome browser on macOS.
6
- */
7
- export const USER_AGENT =
8
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
9
-
10
- /**
11
- * Pre-configured HTTP client with sensible defaults.
12
- * Uses ky for automatic retries, better error handling, and cleaner API.
13
- */
14
- export const http = ky.create({
15
- headers: {
16
- "User-Agent": USER_AGENT,
17
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
18
- "Accept-Language": "en-US,en;q=0.5",
19
- },
20
- timeout: 30000,
21
- retry: {
22
- limit: 2,
23
- statusCodes: [408, 413, 429, 500, 502, 503, 504],
24
- },
25
- });
26
-
27
- /**
28
- * HTTP client configured for JSON APIs.
29
- */
30
- export const httpJson = http.extend({
31
- headers: {
32
- Accept: "application/json",
33
- },
34
- });
@@ -1,6 +0,0 @@
1
- export * from "./auth.js";
2
- export * from "./firebase.js";
3
- export * from "./fs.js";
4
- export * from "./http.js";
5
- export * from "./slug.js";
6
- export * from "./url.js";
@@ -1,122 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { extractQueryParams, getBaseUrl, resolveUrl, resolveUrlWithParams } from "./url.js";
3
-
4
- describe("extractQueryParams", () => {
5
- it("extracts query string with leading ?", () => {
6
- expect(extractQueryParams("https://example.com/path?foo=bar")).toBe("?foo=bar");
7
- });
8
-
9
- it("extracts multiple query params", () => {
10
- expect(extractQueryParams("https://example.com?a=1&b=2&c=3")).toBe("?a=1&b=2&c=3");
11
- });
12
-
13
- it("returns empty string when no query params", () => {
14
- expect(extractQueryParams("https://example.com/path")).toBe("");
15
- });
16
-
17
- it("returns empty string for empty input", () => {
18
- expect(extractQueryParams("")).toBe("");
19
- });
20
-
21
- it("handles URL with fragment after query", () => {
22
- expect(extractQueryParams("https://example.com?foo=bar#section")).toBe("?foo=bar#section");
23
- });
24
-
25
- it("handles query string only (no path)", () => {
26
- expect(extractQueryParams("?token=abc123")).toBe("?token=abc123");
27
- });
28
- });
29
-
30
- describe("getBaseUrl", () => {
31
- it("extracts base URL up to last slash", () => {
32
- expect(getBaseUrl("https://cdn.example.com/videos/playlist.m3u8")).toBe(
33
- "https://cdn.example.com/videos/"
34
- );
35
- });
36
-
37
- it("handles URL with query params", () => {
38
- expect(getBaseUrl("https://cdn.example.com/path/file.ts?token=abc")).toBe(
39
- "https://cdn.example.com/path/"
40
- );
41
- });
42
-
43
- it("handles root URL", () => {
44
- expect(getBaseUrl("https://example.com/")).toBe("https://example.com/");
45
- });
46
-
47
- it("handles URL without trailing path", () => {
48
- // Returns up to last slash (the // in https://)
49
- expect(getBaseUrl("https://example.com")).toBe("https://");
50
- });
51
-
52
- it("handles empty string", () => {
53
- expect(getBaseUrl("")).toBe("");
54
- });
55
-
56
- it("handles deeply nested paths", () => {
57
- expect(getBaseUrl("https://cdn.com/a/b/c/d/file.m3u8")).toBe("https://cdn.com/a/b/c/d/");
58
- });
59
- });
60
-
61
- describe("resolveUrl", () => {
62
- it("returns absolute URL unchanged", () => {
63
- expect(resolveUrl("https://other.com/file.ts", "https://cdn.com/")).toBe(
64
- "https://other.com/file.ts"
65
- );
66
- });
67
-
68
- it("resolves relative URL against base", () => {
69
- expect(resolveUrl("segment001.ts", "https://cdn.example.com/videos/")).toBe(
70
- "https://cdn.example.com/videos/segment001.ts"
71
- );
72
- });
73
-
74
- it("handles http:// URLs", () => {
75
- expect(resolveUrl("http://insecure.com/file.ts", "https://cdn.com/")).toBe(
76
- "http://insecure.com/file.ts"
77
- );
78
- });
79
-
80
- it("handles empty relative URL", () => {
81
- expect(resolveUrl("", "https://cdn.com/path/")).toBe("https://cdn.com/path/");
82
- });
83
-
84
- it("handles relative path with subdirectory", () => {
85
- expect(resolveUrl("sub/segment.ts", "https://cdn.com/videos/")).toBe(
86
- "https://cdn.com/videos/sub/segment.ts"
87
- );
88
- });
89
- });
90
-
91
- describe("resolveUrlWithParams", () => {
92
- it("appends query params to resolved URL", () => {
93
- expect(resolveUrlWithParams("segment.ts", "https://cdn.com/", "?token=abc")).toBe(
94
- "https://cdn.com/segment.ts?token=abc"
95
- );
96
- });
97
-
98
- it("does not duplicate params if URL already has them", () => {
99
- expect(resolveUrlWithParams("segment.ts?existing=1", "https://cdn.com/", "?token=abc")).toBe(
100
- "https://cdn.com/segment.ts?existing=1"
101
- );
102
- });
103
-
104
- it("handles absolute URL with params", () => {
105
- expect(
106
- resolveUrlWithParams("https://other.com/file.ts", "https://cdn.com/", "?token=abc")
107
- ).toBe("https://other.com/file.ts?token=abc");
108
- });
109
-
110
- it("handles empty query params", () => {
111
- expect(resolveUrlWithParams("segment.ts", "https://cdn.com/", "")).toBe(
112
- "https://cdn.com/segment.ts"
113
- );
114
- });
115
-
116
- it("works with complex signed URL tokens", () => {
117
- const token = "?Policy=xxx&Signature=yyy&Key-Pair-Id=zzz";
118
- expect(resolveUrlWithParams("video/720p.m3u8", "https://cdn.com/hls/", token)).toBe(
119
- "https://cdn.com/hls/video/720p.m3u8?Policy=xxx&Signature=yyy&Key-Pair-Id=zzz"
120
- );
121
- });
122
- });
@@ -1,49 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { extractCommunitySlug } from "./database.js";
3
-
4
- describe("extractCommunitySlug", () => {
5
- it("extracts slug from standard Skool URL", () => {
6
- expect(extractCommunitySlug("https://www.skool.com/my-community")).toBe("my-community");
7
- });
8
-
9
- it("extracts slug from Skool URL without www", () => {
10
- expect(extractCommunitySlug("https://skool.com/test-group")).toBe("test-group");
11
- });
12
-
13
- it("extracts slug from URL with path", () => {
14
- expect(extractCommunitySlug("https://www.skool.com/my-community/classroom")).toBe(
15
- "my-community"
16
- );
17
- expect(extractCommunitySlug("https://www.skool.com/my-community/classroom/lessons/123")).toBe(
18
- "my-community"
19
- );
20
- });
21
-
22
- it("extracts slug from URL with query params (includes params in slug)", () => {
23
- // Note: current implementation doesn't strip query params
24
- expect(extractCommunitySlug("https://www.skool.com/my-community?ref=abc")).toBe(
25
- "my-community?ref=abc"
26
- );
27
- });
28
-
29
- it("handles complex community names", () => {
30
- expect(extractCommunitySlug("https://skool.com/the-best-community-ever-2024")).toBe(
31
- "the-best-community-ever-2024"
32
- );
33
- });
34
-
35
- it("returns 'unknown' for non-Skool URLs", () => {
36
- expect(extractCommunitySlug("https://example.com/path")).toBe("unknown");
37
- expect(extractCommunitySlug("https://youtube.com/channel/abc")).toBe("unknown");
38
- });
39
-
40
- it("returns 'unknown' for invalid URLs", () => {
41
- expect(extractCommunitySlug("not-a-url")).toBe("unknown");
42
- expect(extractCommunitySlug("")).toBe("unknown");
43
- });
44
-
45
- it("returns 'unknown' for Skool root URL", () => {
46
- expect(extractCommunitySlug("https://www.skool.com/")).toBe("unknown");
47
- expect(extractCommunitySlug("https://www.skool.com")).toBe("unknown");
48
- });
49
- });