website-api 1.0.6 → 1.1.2

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 (84) hide show
  1. package/dist/bin/cli.js +1 -1
  2. package/dist/src/capabilities/browser.d.ts +23 -0
  3. package/dist/src/capabilities/browser.js +1 -0
  4. package/dist/src/capabilities/cookies.d.ts +24 -0
  5. package/dist/src/capabilities/cookies.js +1 -0
  6. package/dist/src/capabilities/download.d.ts +11 -0
  7. package/dist/src/capabilities/download.js +1 -0
  8. package/dist/src/capabilities/fingerprint.d.ts +14 -0
  9. package/dist/src/capabilities/fingerprint.js +1 -0
  10. package/dist/src/capabilities/http.d.ts +18 -0
  11. package/dist/src/capabilities/http.js +1 -0
  12. package/dist/src/{util → capabilities/login}/login-helper.d.ts +12 -5
  13. package/dist/src/capabilities/login/login-helper.js +1 -0
  14. package/dist/src/capabilities/login/login-strategy.d.ts +15 -0
  15. package/dist/src/capabilities/login/login-strategy.js +1 -0
  16. package/dist/src/cli/ext.d.ts +3 -0
  17. package/dist/src/cli/ext.js +1 -0
  18. package/dist/src/core/context.d.ts +24 -0
  19. package/dist/src/core/context.js +1 -0
  20. package/dist/src/core/define-site.d.ts +9 -0
  21. package/dist/src/core/define-site.js +1 -0
  22. package/dist/src/core/loader.d.ts +21 -0
  23. package/dist/src/core/loader.js +1 -0
  24. package/dist/src/core/registry.d.ts +116 -0
  25. package/dist/src/core/registry.js +1 -0
  26. package/dist/src/core/runtime.d.ts +20 -0
  27. package/dist/src/core/runtime.js +1 -0
  28. package/dist/src/{website → sites}/chase.com/download-helper.d.ts +5 -4
  29. package/dist/src/sites/chase.com/download-helper.js +1 -0
  30. package/dist/src/sites/chase.com/index.d.ts +2 -0
  31. package/dist/src/sites/chase.com/index.js +1 -0
  32. package/dist/src/sites/chatgpt.com/index.d.ts +10 -0
  33. package/dist/src/sites/chatgpt.com/index.js +1 -0
  34. package/dist/src/sites/cursor.com/index.d.ts +6 -0
  35. package/dist/src/sites/cursor.com/index.js +1 -0
  36. package/dist/src/sites/gemini.google.com/index.d.ts +5 -0
  37. package/dist/src/sites/gemini.google.com/index.js +1 -0
  38. package/dist/src/sites/google.com/google-helpers.d.ts +24 -0
  39. package/dist/src/sites/google.com/google-helpers.js +1 -0
  40. package/dist/src/sites/google.com/index.d.ts +2 -0
  41. package/dist/src/sites/google.com/index.js +1 -0
  42. package/dist/src/sites/ollama.com/index.d.ts +9 -0
  43. package/dist/src/sites/ollama.com/index.js +1 -0
  44. package/dist/src/sites/perplexity.ai/index.d.ts +50 -0
  45. package/dist/src/sites/perplexity.ai/index.js +1 -0
  46. package/dist/src/sites/pseg.com/index.d.ts +2 -0
  47. package/dist/src/sites/pseg.com/index.js +1 -0
  48. package/dist/src/sites/pseg.com/pseg-helpers.d.ts +13 -0
  49. package/dist/src/sites/pseg.com/pseg-helpers.js +1 -0
  50. package/dist/src/types.d.ts +194 -46
  51. package/dist/src/util/args-parser.js +1 -1
  52. package/dist/src/website-api.d.ts +9 -34
  53. package/dist/src/website-api.js +1 -1
  54. package/package.json +10 -1
  55. package/dist/src/adapter/base-adapter.d.ts +0 -41
  56. package/dist/src/adapter/base-adapter.js +0 -1
  57. package/dist/src/adapter/playwright-attatch-chrome-adapter.d.ts +0 -16
  58. package/dist/src/adapter/playwright-attatch-chrome-adapter.js +0 -1
  59. package/dist/src/adapter/playwright-core.d.ts +0 -35
  60. package/dist/src/adapter/playwright-core.js +0 -1
  61. package/dist/src/adapter/universal-adapter.d.ts +0 -10
  62. package/dist/src/adapter/universal-adapter.js +0 -1
  63. package/dist/src/util/login-helper.js +0 -1
  64. package/dist/src/website/chase.com/account-helper.d.ts +0 -20
  65. package/dist/src/website/chase.com/account-helper.js +0 -1
  66. package/dist/src/website/chase.com/chase-adapter.d.ts +0 -43
  67. package/dist/src/website/chase.com/chase-adapter.js +0 -1
  68. package/dist/src/website/chase.com/download-helper.js +0 -1
  69. package/dist/src/website/chatgpt.com/chatgpt-adapter.d.ts +0 -11
  70. package/dist/src/website/chatgpt.com/chatgpt-adapter.js +0 -1
  71. package/dist/src/website/cursor.com/cursor-adapter.d.ts +0 -6
  72. package/dist/src/website/cursor.com/cursor-adapter.js +0 -1
  73. package/dist/src/website/example.com/example-adapter.d.ts +0 -12
  74. package/dist/src/website/example.com/example-adapter.js +0 -1
  75. package/dist/src/website/gemini.google.com/gemini-adapter.d.ts +0 -12
  76. package/dist/src/website/gemini.google.com/gemini-adapter.js +0 -1
  77. package/dist/src/website/google.com/google-adapter.d.ts +0 -62
  78. package/dist/src/website/google.com/google-adapter.js +0 -1
  79. package/dist/src/website/ollama.com/ollama-adapter.d.ts +0 -2
  80. package/dist/src/website/ollama.com/ollama-adapter.js +0 -1
  81. package/dist/src/website/perplexity.ai/perplexity-adapter.d.ts +0 -2
  82. package/dist/src/website/perplexity.ai/perplexity-adapter.js +0 -1
  83. package/dist/src/website/pseg.com/pseg-adapter.d.ts +0 -45
  84. package/dist/src/website/pseg.com/pseg-adapter.js +0 -1
package/dist/bin/cli.js CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{readFileSync as o,writeFileSync as e}from"node:fs";import{dirname as n,join as t}from"node:path";import{fileURLToPath as s}from"node:url";import{program as r}from"commander";import i from"chalk";import l from"cli-table3";import{getDefaultChromeDir as a}from"chrome-tools";import{queryWebsite as c,websites as p,loadAdapters as d,getWebsite as f}from"../src/website-api.js";import{parseArgsForWebsite as u}from"../src/util/args-parser.js";const m=t(n(s(import.meta.url)),"..","..","package.json"),{version:g}=JSON.parse(o(m,"utf8"));process.on("unhandledRejection",o=>{console.error(o instanceof Error?o.message:"command not found"),process.exit(1)}),async function(){const o=process.argv.slice(2),n=o.find(o=>!o.startsWith("-"));if(n&&"list"!==n){await d();const t=f(n);if(t){const s=o.filter((e,t)=>t!==o.indexOf(n));let r;try{r=u(t.positionals,t.parameters,s)}catch(o){console.error(i.red(`Error: ${o instanceof Error?o.message:String(o)}`)),console.log(`Run ${i.cyan(`npx website-api ${t.id} --help`)} for usage details.`),process.exit(1)}if(r.helpRequested)return void function(o){console.log(i.bold.green(`\n🌐 Website API: ${i.white(o.name)} (${i.yellow(o.id)})\n`)),console.log(` ${i.italic(o.description)}\n`);let e=`npx website-api ${o.id}`;if(o.positionals&&o.positionals.length>0)for(const n of o.positionals)e+=n.required?` <${n.name}>`:` [${n.name}]`;if(e+=" [options]",console.log(`${i.bold("Usage:")} ${i.cyan(e)}\n`),o.positionals&&o.positionals.length>0){console.log(i.bold("Positional Arguments:"));for(const e of o.positionals)console.log(` ${i.cyan(e.name.padEnd(15))} ${e.description}`);console.log()}console.log(i.bold("Options:"));const n=[...o.parameters||[],{name:"profile",type:"string",description:"specific Chrome profile directory (e.g., 'Default')"},{name:"user-agent",type:"string",description:"custom User-Agent header for HTTP requests",short:"u"},{name:"debug",type:"boolean",description:"Print full HTTP request and response bodies for debugging"},{name:"help",type:"boolean",description:"Show help for this website adapter",short:"h"}];for(const o of n){let e=`--${o.name}`;"boolean"!==o.type&&(e+=" <value>"),e=o.short?`-${o.short}, ${e}`:` ${e}`;const n=void 0!==o.default?` (default: ${o.default})`:"";console.log(` ${i.yellow(e.padEnd(28))} ${o.description}${i.gray(n)}`)}console.log()}(t);try{const o=await c(t.id,r.options);let n;if(r.options.text&&o&&"object"==typeof o){const e=o.answer||o.text;n=void 0!==e?String(e):JSON.stringify(o,null,2)}else n="string"==typeof o?o:JSON.stringify(o,null,2);r.options.out?(e(r.options.out,n+"\n","utf8"),console.log(i.green(`Success! Decoded response written to ${r.options.out}`))):console.log(n)}catch(o){console.error(i.red(o instanceof Error?o.message:"command not found")),process.exit(1)}return}}r.name("website-api").description("CLI to query website APIs using decrypted Chrome cookies on macOS").version(g),r.option("--profile <name>","specific Chrome profile directory (e.g., 'Default', 'Profile 1')").option("--current-profile","Show the currently resolved/selected Chrome profile directory and name").option("-u, --user-agent <string>","custom User-Agent header for HTTP requests"),r.option("--debug","Print full HTTP request and response bodies for debugging"),r.command("list").description("List all supported website API adapters").action(async()=>{await d(),console.log(i.bold.green("\n🌐 Supported Website APIs:\n"));const o=new l({head:[i.bold.cyan("ID"),i.bold.cyan("Name"),i.bold.cyan("Domain"),i.bold.cyan("Description")],colWidths:[18,25,20,50],wordWrap:!0,style:{head:[],border:[]}});for(const e of p)o.push([i.yellow(e.id),e.name,i.underline(e.domain),e.description]);console.log(o.toString()),console.log(`\nTo run an API query, execute: ${i.bold.cyan("npx website-api <id>")}\n`)}),r.argument("[website]","website ID or domain to query (e.g. 'chatgpt.com')").action(async o=>{const e=r.opts();if(e.currentProfile){const o=process.env.PROFILE_PATH||process.env.CHROME_PROFILE_PATH||a(),n=e.profile||process.env.PROFILE_NAME||"Default";return console.log(i.bold.green("\n👤 Currently Resolved Profile:\n")),console.log(` ${i.bold("Path:")} ${o}`),void console.log(` ${i.bold("Name:")} ${n}\n`)}o?(console.error(i.red(`Error: website adapter "${o}" not found.`)),console.log(`Run ${i.cyan("npx website-api list")} to see all supported adapters.`),process.exit(1)):r.outputHelp()}),r.parse(process.argv)}().catch(o=>{console.error(o instanceof Error?o.message:"command not found"),process.exit(1)});
2
+ import{readFileSync as e,writeFileSync as o}from"node:fs";import{dirname as n,join as s}from"node:path";import{fileURLToPath as t}from"node:url";import{program as r}from"commander";import i from"chalk";import l from"cli-table3";import{getDefaultChromeDir as a}from"chrome-tools";import{queryWebsite as c,sites as p,loadSites as d,getSite as u}from"../src/website-api.js";import{registerExtCommands as g}from"../src/cli/ext.js";import{parseArgsForWebsite as f}from"../src/util/args-parser.js";const m=s(n(t(import.meta.url)),"..","..","package.json"),{version:b}=JSON.parse(e(m,"utf8"));process.on("unhandledRejection",e=>{console.error(e instanceof Error?e.message:"command not found"),process.exit(1)}),async function(){const e=process.argv.slice(2),n=e.find(e=>!e.startsWith("-"));if(n&&"list"!==n){await d();const s=u(n);if(s){const t=e.filter((o,s)=>s!==e.indexOf(n));let r;try{r=f(s.positionals,s.parameters,t)}catch(e){console.error(i.red(`Error: ${e instanceof Error?e.message:String(e)}`)),console.log(`Run ${i.cyan(`npx website-api ${s.id} --help`)} for usage details.`),process.exit(1)}if(r.helpRequested)return void function(e){console.log(i.bold.green(`\n🌐 Website API: ${i.white(e.name)} (${i.yellow(e.id)})\n`)),console.log(` ${i.italic(e.description)}\n`);let o=`npx website-api ${e.id}`;if(e.positionals&&e.positionals.length>0)for(const n of e.positionals)o+=n.required?` <${n.name}>`:` [${n.name}]`;if(o+=" [options]",console.log(`${i.bold("Usage:")} ${i.cyan(o)}\n`),e.positionals&&e.positionals.length>0){console.log(i.bold("Positional Arguments:"));for(const o of e.positionals)console.log(` ${i.cyan(o.name.padEnd(15))} ${o.description}`);console.log()}console.log(i.bold("Options:"));const n=[...e.parameters||[],{name:"profile",type:"string",description:"specific Chrome profile directory (e.g., 'Default')"},{name:"user-agent",type:"string",description:"custom User-Agent header for HTTP requests",short:"u"},{name:"debug",type:"boolean",description:"Print full HTTP request and response bodies for debugging"},{name:"keep-open",type:"boolean",description:"Leave the browser tab open after running (preserve the logged-in session)"},{name:"help",type:"boolean",description:"Show help for this website site",short:"h"}];for(const e of n){let o=`--${e.name}`;"boolean"!==e.type&&(o+=" <value>"),o=e.short?`-${e.short}, ${o}`:` ${o}`;const n=void 0!==e.default?` (default: ${e.default})`:"";console.log(` ${i.yellow(o.padEnd(28))} ${e.description}${i.gray(n)}`)}console.log()}(s);try{const e=await c(s.id,r.options);let n;if(r.options.text&&e&&"object"==typeof e){const o=e.answer||e.text;n=void 0!==o?String(o):JSON.stringify(e,null,2)}else n="string"==typeof e?e:JSON.stringify(e,null,2);r.options.out?(o(r.options.out,n+"\n","utf8"),console.log(i.green(`Success! Decoded response written to ${r.options.out}`))):console.log(n)}catch(e){console.error(i.red(e instanceof Error?e.message:"command not found")),process.exit(1)}return}}r.name("website-api").description("CLI to query website APIs using decrypted Chrome cookies on macOS").version(b),r.option("--profile <name>","specific Chrome profile directory (e.g., 'Default', 'Profile 1')").option("--current-profile","Show the currently resolved/selected Chrome profile directory and name").option("-u, --user-agent <string>","custom User-Agent header for HTTP requests"),r.option("--debug","Print full HTTP request and response bodies for debugging"),r.command("list").description("List all supported website API sites").action(async()=>{await d(),console.log(i.bold.green("\n🌐 Supported Website APIs:\n"));const e=new l({head:[i.bold.cyan("ID"),i.bold.cyan("Name"),i.bold.cyan("Domain"),i.bold.cyan("Description")],colWidths:[18,25,20,50],wordWrap:!0,style:{head:[],border:[]}});for(const o of p){const n=[];"browser"===o.transport&&n.push(i.magenta("[p]")),o.auth&&n.push(i.red("[l]")),"extension"===o.origin&&n.push(i.blue("[x]"));const s=n.length?`${o.name} ${n.join(" ")}`:o.name;e.push([i.yellow(o.id),s,i.underline(o.domain),o.description])}console.log(e.toString()),console.log(`\n${i.magenta("[p]")} requires a running Chrome (Playwright) ${i.red("[l]")} requires login ${i.blue("[x]")} user extension`),console.log(`\nTo run an API query, execute: ${i.bold.cyan("npx website-api <id>")}\n`)}),g(r),r.argument("[website]","website ID or domain to query (e.g. 'chatgpt.com')").action(async e=>{const o=r.opts();if(o.currentProfile){const e=process.env.PROFILE_PATH||process.env.CHROME_PROFILE_PATH||a(),n=o.profile||process.env.PROFILE_NAME||"Default";return console.log(i.bold.green("\n👤 Currently Resolved Profile:\n")),console.log(` ${i.bold("Path:")} ${e}`),void console.log(` ${i.bold("Name:")} ${n}\n`)}e?(console.error(i.red(`Error: website adapter "${e}" not found.`)),console.log(`Run ${i.cyan("npx website-api list")} to see all supported adapters.`),process.exit(1)):r.outputHelp()}),r.parse(process.argv)}().catch(e=>{console.error(e instanceof Error?e.message:"command not found"),process.exit(1)});
@@ -0,0 +1,23 @@
1
+ import { type Browser, type Page } from "playwright-core";
2
+ export interface BrowserOptions {
3
+ /** CDP endpoint of a running Chrome. Defaults to env or localhost:9222. */
4
+ cdpEndpoint?: string;
5
+ /** Close a tab opened by this session on dispose. Defaults to true. */
6
+ close?: boolean;
7
+ debug?: boolean;
8
+ }
9
+ export interface BrowserSession {
10
+ page: Page;
11
+ browser: Browser;
12
+ /** Whether this session opened a brand-new tab (vs. reusing one). */
13
+ opened: boolean;
14
+ dispose(): Promise<void>;
15
+ }
16
+ /** A connector function — injectable so the context/tests can swap it out. */
17
+ export type BrowserConnector = (targetUrl: string, options: BrowserOptions) => Promise<BrowserSession>;
18
+ /**
19
+ * Connects to an existing Chrome over CDP and reuses (or opens) a tab for the
20
+ * target URL. Returns a session with an explicit `dispose()` the runtime calls
21
+ * during teardown — sites never manage the connection themselves.
22
+ */
23
+ export declare const connectChrome: BrowserConnector;
@@ -0,0 +1 @@
1
+ import{chromium as t}from"playwright-core";export const connectChrome=async(e,o={})=>{const n=o.cdpEndpoint||process.env.CDP_ENDPOINT||"http://localhost:9222",r=!!o.debug,a=await t.connectOverCDP(n),c=a.contexts()[0];if(!c)throw new Error("No active browser context found. Is Chrome running with remote debugging enabled?");let s=!1,i=c.pages().find(t=>{try{const o=new URL(e).hostname.replace("www.","");return new URL(t.url()).hostname.endsWith(o)||t.url().startsWith(e)}catch{return t.url().startsWith(e)}});return i?r&&console.log(`Reusing existing tab for ${e}`):(r&&console.log(`Opening a new tab for ${e}`),i=await c.newPage(),await i.goto(e,{waitUntil:"domcontentloaded"}),s=!0),{page:i,browser:a,opened:s,async dispose(){if(s&&!1!==o.close)try{await i.close()}catch{}try{await a.close()}catch{}}}};
@@ -0,0 +1,24 @@
1
+ import { getCookies as realGetCookies, getPasswords as realGetPasswords, type CookieEntry } from "chrome-tools";
2
+ import type { Credentials, QueryOptions } from "../types.js";
3
+ export declare const DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36";
4
+ /**
5
+ * Injectable Chrome accessors. Defaults to the real `chrome-tools` functions;
6
+ * tests substitute fakes so the capability is exercised without a real Chrome.
7
+ */
8
+ export interface CookieProviders {
9
+ getCookies?: typeof realGetCookies;
10
+ getPasswords?: typeof realGetPasswords;
11
+ env?: NodeJS.ProcessEnv;
12
+ }
13
+ export declare function resolveUserAgent(options: QueryOptions, env?: NodeJS.ProcessEnv): string;
14
+ export declare function buildCookieString(cookies: CookieEntry[]): string;
15
+ /**
16
+ * Resolves saved Chrome credentials for a domain. Searches the full domain
17
+ * first, then falls back to the registrable name (e.g. "pseg" from "pseg.com").
18
+ */
19
+ export declare function resolveCredentials(domain: string, options: QueryOptions, providers?: CookieProviders): Credentials;
20
+ /**
21
+ * Resolves decrypted Chrome cookies for a domain. When `required` is false a
22
+ * missing login yields an empty array instead of throwing.
23
+ */
24
+ export declare function resolveCookies(domain: string, options: QueryOptions, required: boolean, providers?: CookieProviders): CookieEntry[];
@@ -0,0 +1 @@
1
+ import{getCookies as e,getPasswords as r}from"chrome-tools";export const DEFAULT_USER_AGENT="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36";function o(e,r){return e.profilePath||r.PROFILE_PATH||r.CHROME_PROFILE_PATH||void 0}function n(e,r){return e.profile||r.PROFILE_NAME||void 0}export function resolveUserAgent(e,r=process.env){return e.userAgent||r.userAgent||r.USER_AGENT||DEFAULT_USER_AGENT}export function buildCookieString(e){return e.map(e=>`${e.name}=${e.value}`).join("; ")}export function resolveCredentials(e,t,s={}){const i=s.env??process.env,a=s.getPasswords??r,c=o(t,i),l=n(t,i);let p=a({chromeDir:c,profile:l,search:e});if(!p||0===p.length){const r=e.split(".");p=a({chromeDir:c,profile:l,search:r[r.length-2]||e})}if(!p||0===p.length)throw new Error(`No saved passwords found in Chrome for '${e}'`);const{username:u,password:f}=p[0];if(!u||!f)throw new Error(`Found credentials for '${e}' but username or password was empty`);return{username:u,password:f}}export function resolveCookies(r,t,s,i={}){const a=i.env??process.env,c=i.getCookies??e,l=o(t,a),p=n(t,a);let u=[];try{u=c({chromeDir:l,profile:p,domain:r,decrypt:!0})}catch{if(s)throw new Error("No login found in browser")}if((!u||0===u.length)&&s)throw new Error("No login found in browser");return u??[]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Builds a `save(filename, content)` function bound to a target directory.
3
+ * Creates the directory on first write and returns the absolute path written.
4
+ * `outDir` of null means "current working directory".
5
+ */
6
+ export declare function createSaver(outDir: string | null, cwd?: string): (filename: string, content: string | Buffer) => Promise<string>;
7
+ /**
8
+ * Guards a downloaded payload that should be data but may be an HTML error page
9
+ * (a common symptom of an expired session). Throws a clear, actionable error.
10
+ */
11
+ export declare function assertNotHtml(text: string, label: string): string;
@@ -0,0 +1 @@
1
+ import t from"node:fs/promises";import e from"node:path";export function createSaver(r,o=process.cwd()){const n=e.resolve(o,r??".");let i=!1;return async function(r,o){i||(await t.mkdir(n,{recursive:!0}),i=!0);const s=e.join(n,e.basename(r));return await t.writeFile(s,o),s}}export function assertNotHtml(t,e){const r=String(t??"").trimStart();if(/^<!doctype html/i.test(r)||/^<html/i.test(r))throw new Error(`Download for ${e} returned HTML instead of data. The session may have expired.`);return t}
@@ -0,0 +1,14 @@
1
+ import type { Page } from "playwright-core";
2
+ import type { FingerprintConfig, FingerprintOption } from "../types.js";
3
+ /**
4
+ * Builds the init script that shapes the page's fingerprint. Pure and exported
5
+ * so it can be unit-tested without a real browser.
6
+ */
7
+ export declare function buildFingerprintScript(config: FingerprintConfig): string;
8
+ /** Resolves the effective fingerprint config from the site's option. */
9
+ export declare function resolveFingerprint(option: FingerprintOption, userAgent?: string): FingerprintConfig | null;
10
+ /**
11
+ * Applies the fingerprint to a page via an init script (runs before page
12
+ * scripts on the next navigation). No-op when fingerprinting is disabled.
13
+ */
14
+ export declare function applyFingerprint(page: Page, option: FingerprintOption, userAgent?: string): Promise<void>;
@@ -0,0 +1 @@
1
+ const n={languages:["en-US","en"]};export function buildFingerprintScript(n){return`(() => {\n const cfg = ${JSON.stringify(n)};\n const def = (obj, prop, get) => {\n try { Object.defineProperty(obj, prop, { get, configurable: true }); } catch (_) {}\n };\n // Hide the automation flag.\n def(navigator, "webdriver", () => undefined);\n if (cfg.languages) {\n def(navigator, "languages", () => cfg.languages);\n }\n if (cfg.platform) def(navigator, "platform", () => cfg.platform);\n if (typeof cfg.hardwareConcurrency === "number") {\n def(navigator, "hardwareConcurrency", () => cfg.hardwareConcurrency);\n }\n if (typeof cfg.deviceMemory === "number") {\n def(navigator, "deviceMemory", () => cfg.deviceMemory);\n }\n // Present a non-empty plugins list, as headless/automation often reports none.\n try {\n if (!navigator.plugins || navigator.plugins.length === 0) {\n def(navigator, "plugins", () => [1, 2, 3, 4, 5]);\n }\n } catch (_) {}\n // Ensure window.chrome exists, as some sites probe for it.\n try {\n if (!window.chrome) { window.chrome = { runtime: {} }; }\n } catch (_) {}\n })();`}export function resolveFingerprint(e,r){if(!1===e)return null;const t="stealth"===e?{...n}:{...n,...e};return r&&!t.userAgent&&(t.userAgent=r),t}export async function applyFingerprint(n,e,r){const t=resolveFingerprint(e,r);t&&await n.addInitScript(buildFingerprintScript(t))}
@@ -0,0 +1,18 @@
1
+ import type { HttpCapability } from "../types.js";
2
+ /**
3
+ * Dependencies the HTTP capability needs from the surrounding context. Kept
4
+ * narrow so tests can drive it with a fake fetch + static session values.
5
+ */
6
+ export interface HttpDeps {
7
+ fetchImpl?: typeof fetch;
8
+ cookieString: () => string;
9
+ userAgent: () => string;
10
+ debug?: boolean;
11
+ }
12
+ /** Parses an SSE body into the JSON payloads of its `data:` frames. */
13
+ export declare function parseSSE(raw: string): any[];
14
+ /**
15
+ * Builds the HTTP capability. Every request merges the resolved Chrome cookie
16
+ * string and User-Agent unless the caller already set those headers.
17
+ */
18
+ export declare function createHttp(deps: HttpDeps): HttpCapability;
@@ -0,0 +1 @@
1
+ export function parseSSE(t){const e=[],s=t.replace(/\r\n/g,"\n");for(const t of s.split("\n\n")){const s=[];for(const e of t.split("\n"))e.startsWith("data:")&&s.push(e.slice(5).trimStart());if(s.length)try{const t=JSON.parse(s.join("\n"));t&&("object"!=typeof t||Object.keys(t).length>0)&&e.push(t)}catch{}}return e}export function createHttp(t){const e=t.fetchImpl??fetch;function s(e){const s=new Headers(e?.headers);if(!s.has("Cookie")){const e=t.cookieString();e&&s.set("Cookie",e)}return s.has("User-Agent")||s.set("User-Agent",t.userAgent()),s}async function n(n,a){const r={...a,headers:s(a)};t.debug&&console.log("[debug] Request:",{url:n,init:r});const o=await e(n,r),c=await o.text();if(t.debug&&console.log("[debug] Response:",{url:n,status:o.status,statusText:o.statusText,headers:Array.from(o.headers.entries()),body:c}),!o.ok)throw new Error(`HTTP ${o.status}: ${o.statusText}`);return{response:o,text:c}}return{raw:n,text:async(t,e)=>(await n(t,e)).text,async html(t,e){const s=new Headers(e?.headers);return s.has("Accept")||s.set("Accept","text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"),(await n(t,{...e,headers:s})).text},async json(t,e){const s=new Headers(e?.headers);s.has("Accept")||s.set("Accept","application/json, text/plain, */*");const{text:a}=await n(t,{...e,headers:s});try{return JSON.parse(a)}catch{return{response:a}}},async sse(t,e){const s=new Headers(e?.headers);s.has("Accept")||s.set("Accept","text/event-stream");const{response:a,text:r}=await n(t,{...e,headers:s});return{status:a.status,contentType:a.headers.get("content-type")||"",frames:parseSSE(r),raw:r}}}}
@@ -2,10 +2,17 @@ import type { Page } from "playwright-core";
2
2
  export interface LoginOptions {
3
3
  page: Page;
4
4
  emailSelector: string;
5
- emailValue: string;
6
5
  passwordSelector: string;
7
- passwordValue: string;
8
6
  submitButtonSelector: string;
7
+ usernameSelectors?: string[];
8
+ passwordSelectors?: string[];
9
+ submitSelectors?: string[];
10
+ emailValue?: string;
11
+ passwordValue?: string;
12
+ getCredentials?: () => {
13
+ username: string;
14
+ password: string;
15
+ };
9
16
  delayMs?: number;
10
17
  intendedUrl?: string;
11
18
  loginUrl?: string;
@@ -15,8 +22,8 @@ export interface LoginOptions {
15
22
  dashboardSelectors?: string[];
16
23
  }
17
24
  /**
18
- * Reusable utility to automate form logins via Playwright.
19
- * Handles checking if login is required, filling credentials, waiting a custom delay,
20
- * clicking the submit button, and waiting for redirection.
25
+ * Automates a form login via Playwright: detects whether login is required,
26
+ * fills credentials, waits, submits, and awaits redirection. Idempotent when
27
+ * a dashboard is already showing it skips login and returns false.
21
28
  */
22
29
  export declare function performFormLogin(options: LoginOptions): Promise<boolean>;
@@ -0,0 +1 @@
1
+ const t=['input[type="password"]'],e=['button[type="submit"]','input[type="submit"]'];async function i(t,e){for(const i of e)try{if(await t.locator(i).first().isVisible())return i}catch{}return null}function o(t,...e){const i=[t,...e.flatMap(t=>t??[])];return Array.from(new Set(i.filter(Boolean)))}export async function performFormLogin(a){const{page:r,emailSelector:n,passwordSelector:l,submitButtonSelector:s,delayMs:c=1e3,intendedUrl:w,expectedRedirectUrlPattern:f=w,debug:g=!1,pwdSelector:d='input[type="password"]',dashboardSelectors:u=[]}=a;if(w){if(!r.url().includes(w)){g&&console.log(`[LoginHelper] Navigating to intended URL: ${w}`);try{await r.goto(w,{waitUntil:"domcontentloaded"})}catch(t){g&&console.warn(`[LoginHelper] Warning navigating to ${w}:`,t)}}}try{await r.waitForLoadState("domcontentloaded")}catch{}let p=!1,m=r;g&&console.log("[LoginHelper] Waiting for session state to settle...");const b=Date.now();let y=!1;for(;Date.now()-b<15e3;){let t=!1;for(const e of u)try{if(await r.locator(e).first().isVisible()){p=!1,t=!0;break}}catch{}if(t){y=!0;break}try{if(await r.locator(d).first().isVisible()){p=!0,m=r,y=!0;break}}catch{}let e=!1;for(const t of r.frames())try{if(await t.locator(d).first().isVisible()){p=!0,m=t,e=!0;break}}catch{}if(e){y=!0;break}await r.waitForTimeout(500)}if(!y)try{await r.waitForSelector(n,{state:"visible",timeout:1e3}),p=!0,m=r}catch{try{await r.waitForSelector(l,{state:"visible",timeout:1e3}),p=!0,m=r}catch{p=!1}}if(!p)return g&&console.log("[LoginHelper] Already logged in. Skipping login flow."),!1;let L=a.emailValue,h=a.passwordValue;if((!L||!h)&&a.getCredentials){const t=a.getCredentials();L=t.username,h=t.password}if(!L||!h)throw new Error("[LoginHelper] Login required but no credentials were provided");const S=await i(m,o(n,a.usernameSelectors))||n,k=await i(m,o(l,a.passwordSelectors,t))||l,H=await i(m,o(s,a.submitSelectors,e))||s;if(g&&console.log(`[LoginHelper] Login form detected. email='${S}', password='${k}', submit='${H}'`),await m.fill(S,L),await m.fill(k,h),c>0&&(g&&console.log(`[LoginHelper] Waiting ${c}ms before submission...`),await m.waitForTimeout(c)),g&&console.log(`[LoginHelper] Clicking submit '${H}'...`),await m.click(H),f){g&&console.log(`[LoginHelper] Waiting for redirection matching '${f}'...`);try{await r.waitForURL(f,{timeout:15e3})}catch{try{await r.waitForNavigation({waitUntil:"networkidle",timeout:8e3})}catch{}}}return!0}
@@ -0,0 +1,15 @@
1
+ import type { FormLoginConfig, LoginContext, LoginStrategy } from "../../types.js";
2
+ /**
3
+ * Standard username/password form login. Wraps the universal `performFormLogin`
4
+ * engine, which auto-detects whether the form is present and skips login when a
5
+ * dashboard is already showing.
6
+ */
7
+ export declare class FormLoginStrategy implements LoginStrategy {
8
+ private readonly config;
9
+ constructor(config: FormLoginConfig);
10
+ ensureLoggedIn(ctx: LoginContext): Promise<boolean>;
11
+ }
12
+ /** Type guard: distinguishes a ready strategy from a plain config object. */
13
+ export declare function isLoginStrategy(value: unknown): value is LoginStrategy;
14
+ /** Normalizes `auth` (config or strategy) into a LoginStrategy. */
15
+ export declare function toLoginStrategy(auth: LoginStrategy | FormLoginConfig): LoginStrategy;
@@ -0,0 +1 @@
1
+ import{performFormLogin as e}from"./login-helper.js";export class FormLoginStrategy{config;constructor(e){this.config=e}ensureLoggedIn(t){return e({...this.config,page:t.page,debug:t.debug,getCredentials:t.getCredentials})}}export function isLoginStrategy(e){return"object"==typeof e&&null!==e&&"function"==typeof e.ensureLoggedIn}export function toLoginStrategy(e){return isLoginStrategy(e)?e:new FormLoginStrategy(e)}
@@ -0,0 +1,3 @@
1
+ import type { Command } from "commander";
2
+ /** Registers the `ext` command group on the given Commander program. */
3
+ export declare function registerExtCommands(program: Command): void;
@@ -0,0 +1 @@
1
+ import{createInterface as e}from"node:readline/promises";import{stdin as o,stdout as n}from"node:process";import i from"chalk";import t from"cli-table3";import{loadSites as s,getSite as r}from"../core/runtime.js";import{addRegistry as a,installEntry as l,listInstalled as c,loadIndex as d,removeInstalled as g,removeRegistry as m,resolveEntry as y,resolveRegistries as p,searchRegistries as h}from"../core/registry.js";async function f(t){if(!o.isTTY)return!1;const s=e({input:o,output:n});try{const e=(await s.question(`${t} ${i.gray("[y/N]")} `)).trim().toLowerCase();return"y"===e||"yes"===e}finally{s.close()}}function u(e){console.error(i.red(`Error: ${e}`)),process.exit(1)}export function registerExtCommands(e){const o=e.command("ext").description("Discover and install website extensions from a public registry");o.command("search [query]").description("Search configured registries for installable sites").option("--refresh","Bypass the cached index and re-fetch").action(async(e,o)=>{const n=await h(e??"",{refresh:o.refresh});if(0===n.length)return void console.log(i.yellow(`No sites${e?` matching "${e}"`:""} found.`));const s=new t({head:[i.bold.cyan("ID"),i.bold.cyan("Name"),i.bold.cyan("Domain"),i.bold.cyan("Registry"),i.bold.cyan("Description")],colWidths:[16,22,18,14,44],wordWrap:!0,style:{head:[],border:[]}});for(const e of n){const o=[];"browser"===e.transport&&o.push(i.magenta("[p]")),e.auth&&o.push(i.red("[l]"));const n=o.length?`${e.name} ${o.join(" ")}`:e.name;s.push([i.yellow(e.id),n,i.underline(e.domain),e.registry.name,e.description])}console.log(s.toString()),console.log(`\nInstall one with: ${i.bold.cyan("npx website-api ext install <id>")}\n`)}),o.command("info <id>").description("Show full catalog details for a single site").option("--registry <name>","Disambiguate when multiple registries offer the id").action(async(e,o)=>{const{source:n,entry:t}=await y(e,{registryName:o.registry});console.log(i.bold.green(`\n🌐 ${t.name} ${i.yellow(`(${t.id})`)}\n`)),console.log(` ${i.italic(t.description)}\n`);const s=[["Domain",t.domain],["Registry",`${n.name} (${n.repo})`],["Version",t.version??"—"],["Transport",t.transport??"http"],["Requires login",t.auth?"yes":"no"],["Tags",t.tags?.join(", ")||"—"],["Files",t.files.map(e=>e.name).join(", ")]];for(const[e,o]of s)console.log(` ${i.bold((e+":").padEnd(16))} ${o}`);console.log(`\nInstall with: ${i.bold.cyan(`npx website-api ext install ${t.id}`)}\n`)}),o.command("install <id>").alias("add-site").description("Download and install a site from a registry into your extensions folder").option("--registry <name>","Disambiguate when multiple registries offer the id").option("-y, --yes","Skip the confirmation prompt").option("--refresh","Bypass the cached index and re-fetch").action(async(e,o)=>{const{source:n,index:t,entry:a}=await y(e,{registryName:o.registry,refresh:o.refresh});console.log(i.bold(`\nAbout to install ${i.yellow(a.id)} — ${a.name}`)),console.log(` ${i.bold("Domain:")} ${a.domain}`),console.log(` ${i.bold("Registry:")} ${n.name} (${n.repo}@${t.commit.slice(0,10)})`),console.log(` ${i.bold("Files:")} ${a.files.map(e=>e.name).join(", ")}`),a.auth&&console.log(i.red(` ⚠ This site performs a login and will read saved credentials for ${a.domain}.`)),await s();const c=r(a.id);if("bundled"===c?.origin&&console.log(i.yellow(` ⚠ This will shadow the bundled "${a.id}" site.`)),console.log(i.gray(" ⚠ Installing runs third-party code with your browser session. Only install sites you trust.\n")),!o.yes&&!await f("Install this site?"))return void console.log(i.yellow("Aborted."));const{dir:d}=await l(n,t,a,{refresh:o.refresh});console.log(i.green(`\n✓ Installed ${a.id} → ${d}`)),console.log(`Run it with: ${i.bold.cyan(`npx website-api ${a.id}`)}\n`)}),o.command("list").description("List sites you have installed from registries").action(()=>{const e=c();if(0===e.length)return void console.log(i.yellow("No registry sites installed. Try: ")+i.cyan("npx website-api ext search"));const o=new t({head:[i.bold.cyan("ID"),i.bold.cyan("Version"),i.bold.cyan("Registry"),i.bold.cyan("Commit"),i.bold.cyan("Installed")],style:{head:[],border:[]}});for(const n of e)o.push([i.yellow(n.id),n.version??"—",n.repo,n.commit.slice(0,10),n.installedAt.slice(0,10)]);console.log(o.toString())}),o.command("remove <id>").alias("uninstall").description("Remove an installed registry site").action(e=>{g(e)?console.log(i.green(`✓ Removed ${e}`)):u(`"${e}" is not installed`)}),o.command("update [id]").description("Re-install installed sites whose registry commit has changed").option("-y, --yes","Skip the confirmation prompt").action(async(e,o)=>{const n=c().filter(o=>!e||o.id===e);0===n.length&&u(e?`"${e}" is not installed`:"no registry sites installed");let t=0;for(const e of n){let n;try{n=await y(e.id,{registryName:e.registry,refresh:!0})}catch{console.log(i.yellow(`• ${e.id}: no longer in registry, skipping`));continue}n.index.commit!==e.commit?(console.log(`• ${e.id}: ${e.commit.slice(0,10)} → ${n.index.commit.slice(0,10)}`),(o.yes||await f(` Update ${e.id}?`))&&(await l(n.source,n.index,n.entry),console.log(i.green(` ✓ updated ${e.id}`)),t++)):console.log(i.gray(`• ${e.id}: up to date`))}console.log(t?i.green(`\nUpdated ${t} site(s).`):i.gray("\nNothing to update."))});const n=o.command("registry").description("Manage the registries searched for sites");n.command("list").description("List configured registries (in search priority order)").action(()=>{const e=new t({head:[i.bold.cyan("Name"),i.bold.cyan("Repo"),i.bold.cyan("Branch")],style:{head:[],border:[]}});for(const o of p())e.push([o.name,o.repo,o.branch]);console.log(e.toString())}),n.command("add <spec>").description("Add a registry (owner/repo, owner/repo#branch, or a github.com URL)").action(async e=>{const o=a(e);try{const e=await d(o,{refresh:!0});console.log(i.green(`✓ Added registry ${o.name} (${o.repo}) — ${e.sites.length} site(s) available`))}catch(e){console.log(i.yellow(`Added ${o.repo}, but its index.json could not be fetched: ${e instanceof Error?e.message:String(e)}`))}}),n.command("remove <repoOrName>").description("Remove a configured registry").action(e=>{m(e)?console.log(i.green(`✓ Removed registry ${e}`)):u(`registry "${e}" not found in config`)})}
@@ -0,0 +1,24 @@
1
+ import type { QueryOptions, Site, SiteContext } from "../types.js";
2
+ import { type CookieProviders } from "../capabilities/cookies.js";
3
+ import { type BrowserConnector } from "../capabilities/browser.js";
4
+ /**
5
+ * Injectable dependencies. In production all default to the real
6
+ * implementations; tests pass fakes to exercise a site or capability without a
7
+ * real Chrome, network, or filesystem.
8
+ */
9
+ export interface ContextProviders extends CookieProviders {
10
+ fetchImpl?: typeof fetch;
11
+ connectBrowser?: BrowserConnector;
12
+ cwd?: string;
13
+ }
14
+ /** A context plus the teardown hook the runtime calls in `finally`. */
15
+ export interface ManagedContext {
16
+ ctx: SiteContext;
17
+ dispose(): Promise<void>;
18
+ }
19
+ /**
20
+ * Builds the lazy capability context handed to `site.run(ctx)`. Nothing
21
+ * expensive happens until a capability is touched: cookies are read on first
22
+ * `cookies()`/HTTP call, and Chrome is launched on first `browser()`.
23
+ */
24
+ export declare function createContext(site: Site, options?: QueryOptions, providers?: ContextProviders): ManagedContext;
@@ -0,0 +1 @@
1
+ import{resolve as e}from"node:path";import{buildCookieString as i,resolveCookies as o,resolveCredentials as t,resolveUserAgent as r}from"../capabilities/cookies.js";import{createHttp as a}from"../capabilities/http.js";import{applyFingerprint as s}from"../capabilities/fingerprint.js";import{connectChrome as n}from"../capabilities/browser.js";import{createSaver as c}from"../capabilities/download.js";export function createContext(p,d={},l={}){const u=l.env??process.env,g=!!d.debug,m="required"===p.cookies;let w;const b=()=>(void 0===w&&(w=o(p.domain,d,m,l)),w),f=()=>i(b()),h=()=>r(d,u),k=()=>t(p.domain,d,l),v=a({fetchImpl:l.fetchImpl,cookieString:f,userAgent:h,debug:g}),D=l.connectBrowser??n;let j,y;const I=void 0!==d.close?!!d.close:!(d.keepOpen||p.keepBrowserOpen),x=()=>(y||(y=(async()=>(j=await D(p.landingUrl,{cdpEndpoint:u.CDP_ENDPOINT,close:I,debug:g}),await s(j.page,p.fingerprint,h()),p.auth&&await p.auth.ensureLoggedIn({page:j.page,debug:g,getCredentials:k}),j.page))()),y),C=d.outDir?e(l.cwd??process.cwd(),d.outDir):null,O=c(d.outDir??null,l.cwd);return{ctx:{site:p,domain:p.domain,options:d,debug:g,outDir:C,cookies:b,cookieString:f,credentials:k,userAgent:h,http:v,browser:x,eval:async e=>(await x()).evaluate(e),save:O},async dispose(){if(j)try{await j.dispose()}catch{}}}}
@@ -0,0 +1,9 @@
1
+ import type { Site, SiteDef } from "../types.js";
2
+ /** Detects an already-normalized site so loading is idempotent. */
3
+ export declare function isSite(value: unknown): value is Site;
4
+ /**
5
+ * Normalizes a minimal {@link SiteDef} into a fully-defaulted {@link Site}.
6
+ * Accepts a plain object (the common external-extension case), an already
7
+ * normalized site, or a class instance / factory result with the same fields.
8
+ */
9
+ export declare function defineSite(def: SiteDef): Site;
@@ -0,0 +1 @@
1
+ import{toLoginStrategy as t}from"../capabilities/login/login-strategy.js";export function isSite(t){return"object"==typeof t&&null!==t&&!0===t.__site}export function defineSite(e){if(isSite(e))return e;for(const t of["id","name","domain","description"])if(!e[t]||"string"!=typeof e[t])throw new Error(`Site is missing required string field "${t}"`);const n=Array.isArray(e.endpoints)&&e.endpoints.length>0;if(!e.run&&!n)throw new Error(`Site "${e.id}" must define either "endpoints" or "run"`);const i=e.run?async t=>e.run(t):(r=e.endpoints,async t=>{const e=r[0],n="html"===e.responseType?"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8":"text"===e.responseType?"text/plain,*/*;q=0.8":"application/json, text/plain, */*",i={method:e.method||"GET",headers:{Accept:n,...e.headers}};let o;return o="html"===e.responseType||"text"===e.responseType?await t.http.text(e.url,i):await t.http.json(e.url,i),e.transform?e.transform(o,t):o});var r;return{id:e.id,name:e.name,domain:e.domain,description:e.description,transport:e.transport??"http",cookies:e.cookies??"required",fingerprint:e.fingerprint??"stealth",keepBrowserOpen:e.keepBrowserOpen??!1,auth:e.auth?t(e.auth):void 0,parameters:e.parameters??[],positionals:e.positionals??[],landingUrl:e.endpoints?.[0]?.url??`https://${e.domain}`,run:i,__site:!0}}
@@ -0,0 +1,21 @@
1
+ import type { Site } from "../types.js";
2
+ /** Bundled sites ship inside the package (dist/src/sites). */
3
+ export declare const BUNDLED_SITES_DIR: string;
4
+ /**
5
+ * Resolves the extension roots searched in addition to bundled sites:
6
+ * 1. <config>/website-api/extensions (XDG_CONFIG_HOME or ~/.config)
7
+ * 2. each dir in $WEBSITE_API_EXTENSIONS (colon-separated)
8
+ *
9
+ * These hold the user's own sites, kept entirely outside the source tree.
10
+ */
11
+ export declare function extensionRoots(env?: NodeJS.ProcessEnv): string[];
12
+ /**
13
+ * Discovers and loads all sites from the bundled dir and every extension root.
14
+ * Later roots override earlier ones by `id`, so a user extension can shadow a
15
+ * bundled site. Failures in one file never abort the whole load.
16
+ */
17
+ export declare function discoverSites(options?: {
18
+ bundledDir?: string;
19
+ roots?: string[];
20
+ env?: NodeJS.ProcessEnv;
21
+ }): Promise<Site[]>;
@@ -0,0 +1 @@
1
+ import{existsSync as t,readdirSync as o,statSync as n}from"node:fs";import{homedir as r}from"node:os";import{dirname as e,join as s}from"node:path";import{fileURLToPath as i,pathToFileURL as c}from"node:url";import{defineSite as f,isSite as u}from"./define-site.js";const d=e(i(import.meta.url));export const BUNDLED_SITES_DIR=s(d,"..","sites");export function extensionRoots(t=process.env){const o=[],n=t.XDG_CONFIG_HOME||s(r(),".config");if(o.push(s(n,"website-api","extensions")),t.WEBSITE_API_EXTENSIONS)for(const n of t.WEBSITE_API_EXTENSIONS.split(":"))n.trim()&&o.push(n.trim());return o}function p(t){return!(!t.endsWith(".js")&&!t.endsWith(".mjs"))&&(!t.includes(".test.")&&!t.includes(".d.")&&!/(^|[-.])helper(s)?\.(m?js)$/i.test(t))}function a(t){const n=o(t);for(const o of["index.mjs","index.js"])if(n.includes(o))return s(t,o);const r=n.find(p);return r?s(t,r):null}function l(t){const r=[];let e;try{e=o(t)}catch{return r}for(const o of e){const e=s(t,o);let i;try{i=n(e)}catch{continue}if(i.isDirectory()){const t=a(e);t&&r.push(t)}else p(o)&&r.push(e)}return r}async function m(t,o){const n=await import(c(t).href);let r=n.default??n.site??n.sites;if(null==r)return[];if("function"==typeof r&&!u(r))try{r=new r}catch{try{r=r()}catch{return[]}}const e=Array.isArray(r)?r:[r],s=[];for(const t of e){if(!t||"object"!=typeof t)continue;const n=f(t);n.origin=o,s.push(n)}return s}export async function discoverSites(o={}){const n=o.env??process.env,r=[{dir:o.bundledDir??BUNDLED_SITES_DIR,origin:"bundled"},...(o.roots??extensionRoots(n)).map(t=>({dir:t,origin:"extension"}))],e=new Map;for(const{dir:o,origin:s}of r)if(t(o))for(const t of l(o))try{for(const o of await m(t,s))e.set(o.id,o)}catch(o){n.WEBSITE_API_DEBUG&&console.error(`[loader] failed to load ${t}:`,o)}return Array.from(e.values())}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Remote site registry ("a public directory of installable sites").
3
+ *
4
+ * A registry is just a public Git repo containing a generated `index.json`
5
+ * catalog plus one folder of prebuilt JS per site. Nothing here touches the
6
+ * loader: `install` only downloads files into the extensions root that
7
+ * {@link extensionRoots} already discovers, so an installed site behaves
8
+ * exactly like any hand-placed extension.
9
+ */
10
+ /** A configured registry source. `repo` is "owner/name". */
11
+ export interface RegistrySource {
12
+ name: string;
13
+ repo: string;
14
+ branch: string;
15
+ }
16
+ /** One file of a site, with its expected content hash for integrity. */
17
+ export interface RegistryFile {
18
+ name: string;
19
+ sha256: string;
20
+ }
21
+ /** One catalog entry — mirrors the public Site fields plus install metadata. */
22
+ export interface RegistrySiteEntry {
23
+ id: string;
24
+ name: string;
25
+ domain: string;
26
+ description: string;
27
+ version?: string;
28
+ transport?: "http" | "browser";
29
+ auth?: boolean;
30
+ tags?: string[];
31
+ /** Folder within the repo holding this site's files. */
32
+ path: string;
33
+ /** Files to download; first entry is treated as the module entry point. */
34
+ files: RegistryFile[];
35
+ }
36
+ /** The generated catalog served at the registry repo root. */
37
+ export interface RegistryIndex {
38
+ schemaVersion: number;
39
+ /** Commit the `files[].sha256` hashes were generated against. */
40
+ commit: string;
41
+ updatedAt?: string;
42
+ sites: RegistrySiteEntry[];
43
+ }
44
+ /** Provenance written next to an installed site as `.source.json`. */
45
+ export interface InstalledRecord {
46
+ id: string;
47
+ registry: string;
48
+ repo: string;
49
+ commit: string;
50
+ version?: string;
51
+ installedAt: string;
52
+ }
53
+ /** A catalog entry tagged with the registry it came from. */
54
+ export type FoundEntry = RegistrySiteEntry & {
55
+ registry: RegistrySource;
56
+ };
57
+ /** Pluggable fetch so tests never hit the network. */
58
+ export type FetchImpl = (url: string) => Promise<{
59
+ ok: boolean;
60
+ status: number;
61
+ arrayBuffer(): Promise<ArrayBuffer>;
62
+ text(): Promise<string>;
63
+ }>;
64
+ /** Built-in default registry, used when the user has configured none. */
65
+ export declare const DEFAULT_REGISTRY: RegistrySource;
66
+ interface RegistryOpts {
67
+ env?: NodeJS.ProcessEnv;
68
+ fetchImpl?: FetchImpl;
69
+ refresh?: boolean;
70
+ ttlMs?: number;
71
+ }
72
+ /** `~/.config/website-api/config.json` */
73
+ export declare function configPath(env?: NodeJS.ProcessEnv): string;
74
+ /** Parses "owner/repo", "owner/repo#branch", or a github.com URL. */
75
+ export declare function parseRepoSpec(spec: string): RegistrySource;
76
+ /**
77
+ * The registries to search, in priority order:
78
+ * 1. $WEBSITE_API_REGISTRY (one-off override; may be a comma list)
79
+ * 2. configured registries in config.json
80
+ * 3. the built-in DEFAULT_REGISTRY (always present as a fallback)
81
+ * Later duplicates by `repo` are dropped.
82
+ */
83
+ export declare function resolveRegistries(env?: NodeJS.ProcessEnv): RegistrySource[];
84
+ /** Adds a registry to config.json (idempotent by repo). Returns the source. */
85
+ export declare function addRegistry(spec: string, env?: NodeJS.ProcessEnv): RegistrySource;
86
+ /** Removes a registry from config.json by repo or name. Returns true if removed. */
87
+ export declare function removeRegistry(repoOrName: string, env?: NodeJS.ProcessEnv): boolean;
88
+ /** Loads (and disk-caches with a TTL) one registry's index.json. */
89
+ export declare function loadIndex(source: RegistrySource, opts?: RegistryOpts): Promise<RegistryIndex>;
90
+ /** Searches every configured registry; optional free-text query over id/name/domain/tags. */
91
+ export declare function searchRegistries(query: string, opts?: RegistryOpts): Promise<FoundEntry[]>;
92
+ /**
93
+ * Resolves a single id to its catalog entry. If `registryName` is omitted and
94
+ * more than one registry offers the id, throws and asks the caller to choose.
95
+ */
96
+ export declare function resolveEntry(id: string, opts?: RegistryOpts & {
97
+ registryName?: string;
98
+ }): Promise<{
99
+ source: RegistrySource;
100
+ index: RegistryIndex;
101
+ entry: RegistrySiteEntry;
102
+ }>;
103
+ /**
104
+ * Downloads one site's files (pinned to `index.commit`), verifies each against
105
+ * its sha256, writes them to the extensions root, validates the entry actually
106
+ * loads as a site, and records provenance. Returns the install directory.
107
+ */
108
+ export declare function installEntry(source: RegistrySource, index: RegistryIndex, entry: RegistrySiteEntry, opts?: RegistryOpts): Promise<{
109
+ dir: string;
110
+ record: InstalledRecord;
111
+ }>;
112
+ /** Lists locally installed registry sites (those carrying a `.source.json`). */
113
+ export declare function listInstalled(env?: NodeJS.ProcessEnv): InstalledRecord[];
114
+ /** Removes an installed site directory. Returns true if something was removed. */
115
+ export declare function removeInstalled(id: string, env?: NodeJS.ProcessEnv): boolean;
116
+ export {};
@@ -0,0 +1 @@
1
+ import{createHash as e}from"node:crypto";import{existsSync as t,mkdirSync as r,readdirSync as n,readFileSync as o,rmSync as i,statSync as s,writeFileSync as c}from"node:fs";import{homedir as a}from"node:os";import{dirname as f,join as p}from"node:path";import{pathToFileURL as u}from"node:url";import{defineSite as l}from"./define-site.js";import{extensionRoots as d}from"./loader.js";export const DEFAULT_REGISTRY={name:"guocity",repo:"guocity/website-api-list",branch:"main"};function h(e){return e.XDG_CONFIG_HOME||p(a(),".config")}export function configPath(e=process.env){return p(h(e),"website-api","config.json")}function m(e){return d(e)[0]}export function parseRepoSpec(e){let t=e.trim();t=t.replace(/^https?:\/\/github\.com\//i,"").replace(/^github:/i,""),t=t.replace(/\.git$/i,"").replace(/\/$/,"");let r="main";const n=t.indexOf("#");n>=0&&(r=t.slice(n+1)||"main",t=t.slice(0,n));const o=t.split("/").filter(Boolean);if(o.length<2)throw new Error(`Invalid registry spec "${e}" (expected owner/repo)`);const i=`${o[0]}/${o[1]}`;return{name:o[0],repo:i,branch:r}}function g(e){const r=configPath(e);if(!t(r))return{};try{return JSON.parse(o(r,"utf8"))}catch{return{}}}function w(e,t){const n=configPath(t);r(f(n),{recursive:!0}),c(n,JSON.stringify(e,null,2)+"\n","utf8")}export function resolveRegistries(e=process.env){const t=[],r=new Set,n=e=>{r.has(e.repo)||(r.add(e.repo),t.push(e))};if(e.WEBSITE_API_REGISTRY)for(const t of e.WEBSITE_API_REGISTRY.split(","))t.trim()&&n(parseRepoSpec(t));for(const t of g(e).registries??[])n(t);return n(DEFAULT_REGISTRY),t}export function addRegistry(e,t=process.env){const r=parseRepoSpec(e),n=g(t);return n.registries=(n.registries??[]).filter(e=>e.repo!==r.repo),n.registries.push(r),w(n,t),r}export function removeRegistry(e,t=process.env){const r=g(t),n=r.registries?.length??0;return r.registries=(r.registries??[]).filter(t=>t.repo!==e&&t.name!==e),w(r,t),(r.registries?.length??0)<n}function y(e,t,r){return`https://raw.githubusercontent.com/${e}/${t}/${r}`}function v(){return e=>fetch(e)}async function $(e,t){const r=await t(e);if(!r.ok)throw new Error(`GET ${e} → HTTP ${r.status}`);return Buffer.from(await r.arrayBuffer())}function E(t){return e("sha256").update(t).digest("hex")}export async function loadIndex(e,n={}){const i=n.env??process.env,s=n.fetchImpl??v(),a=n.ttlMs??36e5,u=function(e,t){const r=e.replace(/[^a-z0-9._-]+/gi,"_");return p(h(t),"website-api","cache",`${r}.json`)}(e.repo,i);if(!n.refresh&&t(u))try{const{fetchedAt:e,index:t}=JSON.parse(o(u,"utf8"));if("number"==typeof e&&Date.now()-e<a)return t}catch{}const l=await async function(e,t){const r=await t(e);if(!r.ok)throw new Error(`GET ${e} → HTTP ${r.status}`);return r.text()}(y(e.repo,e.branch,"index.json"),s),d=JSON.parse(l);if(!d||!Array.isArray(d.sites))throw new Error(`Registry ${e.repo} has no valid index.json`);try{r(f(u),{recursive:!0}),c(u,JSON.stringify({fetchedAt:Date.now(),index:d}),"utf8")}catch{}return d}export async function searchRegistries(e,t={}){const r=t.env??process.env,n=e.trim().toLowerCase(),o=[];for(const e of resolveRegistries(r)){let r;try{r=await loadIndex(e,t)}catch{continue}for(const t of r.sites){const r=[t.id,t.name,t.domain,t.description,...t.tags??[]].join(" ").toLowerCase();n&&!r.includes(n)||o.push({...t,registry:e})}}return o}export async function resolveEntry(e,t={}){const r=t.env??process.env,n=[];for(const o of resolveRegistries(r)){if(t.registryName&&o.name!==t.registryName&&o.repo!==t.registryName)continue;let r;try{r=await loadIndex(o,t)}catch{continue}const i=r.sites.find(t=>t.id===e);i&&n.push({source:o,index:r,entry:i})}if(0===n.length)throw new Error(`Site "${e}" not found in any registry`);if(n.length>1){const t=n.map(e=>e.source.name).join(", ");throw new Error(`Site "${e}" is offered by multiple registries (${t}); pass --registry <name>`)}return n[0]}export async function installEntry(e,t,n,o={}){const s=o.env??process.env,a=o.fetchImpl??v();if(!n.files?.length)throw new Error(`Catalog entry "${n.id}" lists no files`);const d=p(m(s),n.id),h=[];for(const r of n.files){const o=y(e.repo,t.commit,`${n.path}/${r.name}`),i=await $(o,a),s=E(i);if(s!==r.sha256)throw new Error(`Integrity check failed for ${n.id}/${r.name} (expected ${r.sha256.slice(0,12)}…, got ${s.slice(0,12)}…)`);h.push({name:r.name,buf:i})}i(d,{recursive:!0,force:!0}),r(d,{recursive:!0});for(const{name:e,buf:t}of h){const n=p(d,e);r(f(n),{recursive:!0}),c(n,t)}await async function(e,t){let r;try{r=await import(u(e).href)}catch(e){throw new Error(`Downloaded site failed to import: ${e instanceof Error?e.message:String(e)}`)}const n=r.default??r.site;if(!n||"object"!=typeof n)throw new Error(`Downloaded ${e} does not default-export a site object`);const o=l(n);if(o.id!==t)throw new Error(`Catalog id "${t}" does not match the site's own id "${o.id}"`)}(p(d,n.files[0].name),n.id);const g={id:n.id,registry:e.name,repo:e.repo,commit:t.commit,version:n.version,installedAt:(new Date).toISOString()};return c(p(d,".source.json"),JSON.stringify(g,null,2)+"\n","utf8"),{dir:d,record:g}}export function listInstalled(e=process.env){const r=m(e);if(!t(r))return[];const i=[];for(const e of n(r)){const t=p(r,e,".source.json");try{s(t).isFile()&&i.push(JSON.parse(o(t,"utf8")))}catch{}}return i}export function removeInstalled(e,r=process.env){const n=p(m(r),e);return!!t(n)&&(i(n,{recursive:!0,force:!0}),!0)}
@@ -0,0 +1,20 @@
1
+ import type { QueryOptions, Site } from "../types.js";
2
+ import { type ContextProviders } from "./context.js";
3
+ /** The loaded site registry, populated by {@link loadSites}. */
4
+ export declare let sites: Site[];
5
+ /** Auto-discovers and loads all sites (bundled + extensions). Idempotent. */
6
+ export declare function loadSites(force?: boolean): Promise<Site[]>;
7
+ /** Test/embedding hook: replace the registry with an explicit set. */
8
+ export declare function setSites(next: Site[]): void;
9
+ /** Finds a site by exact id/domain or a `.com`-insensitive match. */
10
+ export declare function getSite(id: string): Site | null;
11
+ /**
12
+ * Creates a fallback site for any URL/domain without a dedicated definition:
13
+ * a single GET to the URL with optional cookies.
14
+ */
15
+ export declare function createUniversalSite(websiteId: string): Site | null;
16
+ /**
17
+ * Resolves a site (dedicated or universal), runs it with a fresh capability
18
+ * context, and guarantees teardown of any browser session.
19
+ */
20
+ export declare function queryWebsite(websiteId: string, options?: QueryOptions, providers?: ContextProviders): Promise<unknown>;
@@ -0,0 +1 @@
1
+ import{defineSite as t}from"./define-site.js";import{discoverSites as e}from"./loader.js";import{createContext as o}from"./context.js";export let sites=[];let r=!1;export async function loadSites(t=!1){return r&&!t||(sites=await e(),r=!0),sites}export function setSites(t){sites=t,r=!0}export function getSite(t){if(!t)return null;const e=t.toLowerCase().trim();return sites.find(t=>t.id.toLowerCase()===e||t.id.toLowerCase().replace(".com","")===e||t.domain.toLowerCase()===e||t.domain.toLowerCase().replace(".com","")===e)??null}export function createUniversalSite(e){let o=e;o.startsWith("http://")||o.startsWith("https://")||(o="https://"+o);try{const r=new URL(o);return t({id:e,name:e,domain:r.hostname,description:`Universal site for ${r.hostname}`,cookies:"optional",endpoints:[{url:r.href}]})}catch{return null}}export async function queryWebsite(t,e={},r={}){await loadSites();let i=getSite(t);if(!i){if(!function(t){return t.startsWith("http://")||t.startsWith("https://")||t.includes(".")||t.includes("/")||t.includes(":")}(t))throw new Error("command not found");if(i=createUniversalSite(t),!i)throw new Error("command not found")}const{ctx:n,dispose:s}=o(i,e,r);try{return await i.run(n)}finally{await s()}}
@@ -1,4 +1,5 @@
1
- import type { Page } from 'playwright-core';
1
+ import type { Page } from "playwright-core";
2
+ import type { SiteContext } from "../../types.js";
2
3
  export interface ChaseDownloadOptions {
3
4
  list?: boolean;
4
5
  download?: boolean;
@@ -9,12 +10,11 @@ export interface ChaseDownloadOptions {
9
10
  range?: string;
10
11
  from?: string | null;
11
12
  to?: string | null;
12
- outDir?: string | null;
13
13
  accounts?: string | null;
14
14
  }
15
15
  export interface ChaseDownloadAccount {
16
16
  id: string;
17
- summaryType: 'CARD' | 'DDA';
17
+ summaryType: "CARD" | "DDA";
18
18
  detailType: string;
19
19
  nickname: string;
20
20
  mask: string;
@@ -30,4 +30,5 @@ export declare function accountFileName(account: ChaseDownloadAccount): string;
30
30
  export declare function summarizeAccounts(accounts: ChaseDownloadAccount[]): string;
31
31
  export declare function selectAccounts(accounts: ChaseDownloadAccount[], opts: ChaseDownloadOptions): ChaseDownloadAccount[];
32
32
  export declare function fetchAccountCsv(page: Page, account: ChaseDownloadAccount, opts: ChaseDownloadOptions, activityKey: string): Promise<string>;
33
- export declare function downloadAccounts(page: Page, opts: ChaseDownloadOptions): Promise<string>;
33
+ /** Discovers, selects, downloads, and saves (or prints) Chase account CSVs. */
34
+ export declare function downloadAccounts(page: Page, ctx: SiteContext): Promise<string>;
@@ -0,0 +1 @@
1
+ import{assertNotHtml as t}from"../../capabilities/download.js";const e={CARD:{mode:"cardGet",count:"/svc/rr/accounts/secure/gateway/credit-card/transactions/inquiry-maintenance/digital-transaction-activity/v1/transaction-counts",csv:"/svc/rr/accounts/secure/gateway/credit-card/transactions/inquiry-maintenance/digital-transaction-activity/v1/transaction-activities"},DDA:{mode:"formPost",count:"/svc/rr/accounts/secure/v1/account/activity/download/count/dda/list",csv:"/svc/rr/accounts/secure/v1/account/activity/download/dda/list"}},n=new Map([["current",{key:"current",label:"Current display, including filters"}],["current-display",{key:"current",label:"Current display, including filters"}],["all",{key:"all",label:"All transactions"}],["all-transactions",{key:"all",label:"All transactions"}],["date-range",{key:"date-range",label:"Choose a date range"}],["date range",{key:"date-range",label:"Choose a date range"}],["choose-date-range",{key:"date-range",label:"Choose a date range"}]]);export function normalizeActivity(t){const e=String(t||"").trim().toLowerCase(),a=n.get(e||"all");if(!a)throw new Error(`Unknown activity option: ${t}`);return a}export async function fetchDownloadOptions(t){t.url().includes("secure.chase.com")||await t.goto("https://secure.chase.com/web/auth/dashboard#/dashboard/overview",{waitUntil:"domcontentloaded"});const e=await t.evaluate(async t=>{const e=await fetch(t.url,{method:"POST",credentials:"include",headers:t.headers,body:""});return{status:e.status,text:await e.text()}},{url:"/svc/rr/accounts/secure/v1/account/activity/download/options/list",headers:{"content-type":"application/x-www-form-urlencoded; charset=UTF-8","x-jpmc-channel":"id=C30","x-jpmc-csrf-token":"NONE"}});if(e.status<200||e.status>=300)throw new Error("Download options request failed with status "+e.status);return JSON.parse(e.text)}export function collectAccounts(t){return(t.downloadAccountActivityOptions||[]).map(t=>({id:String(t.accountId||"").trim(),summaryType:String(t.summaryType||"").trim(),detailType:String(t.detailType||"").trim(),nickname:String(t.nickName||"").trim(),mask:String(t.mask||"").trim()})).filter(t=>t.id&&("CARD"===t.summaryType||"DDA"===t.summaryType)&&t.detailType)}export function formatAccountLabel(t){const e=[];return t.nickname&&e.push(t.nickname),t.mask&&e.push(`****${t.mask}`),e.length||e.push(`${t.summaryType}/${t.detailType}/${t.id}`),e.join(" ")}export function accountFileName(t){return`${[t.nickname||"account",t.summaryType,t.detailType,t.id].map(t=>String(t).trim().replace(/[^A-Za-z0-9._-]+/g,"-")).filter(Boolean).join("-").replace(/-+/g,"-").replace(/^-|-$/g,"")||"account"}.csv`}export function summarizeAccounts(t){const e=[`Found ${t.length} downloadable account${1===t.length?"":"s"}:`];for(const[n,a]of t.entries())e.push(`${n+1}. ${formatAccountLabel(a)} | ${a.summaryType},${a.detailType},${a.id}`);return e.join("\n")}export function selectAccounts(t,e){let n=[];if(e.accounts&&(n=e.accounts.split(/[\s,]+/).map(Number).filter(t=>!isNaN(t))),!n.length){const n=void 0!==e.limit?e.limit:e.first?1:null;return t.slice(0,n||void 0)}const a=[],o=new Set;for(const e of n){if(e<1||e>t.length)throw new Error(`Account number ${e} is out of range. Run list to see 1-${t.length}.`);o.has(e)||(o.add(e),a.push(t[e-1]))}return a}function a(t){return`${t.getFullYear()}${String(t.getMonth()+1).padStart(2,"0")}${String(t.getDate()).padStart(2,"0")}`}function o(t){const e=String(t||"").match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);if(!e)throw new Error(`Invalid date "${t}". Use mm/dd/yyyy format.`);const[,n,a,o]=e;return`${o}${n.padStart(2,"0")}${a.padStart(2,"0")}`}function r(t,e,n){if("CARD"===t.summaryType){const r=new Date,c=new Date(r);c.setFullYear(r.getFullYear()-2);const s={"account-activity-download-type-code":"CSV","digital-account-identifier":t.id};if("date-range"===n){if(!e.from||!e.to)throw new Error("date-range requires from and to in mm/dd/yyyy format");s["start-date"]=o(e.from),s["end-date"]=o(e.to)}else"all"===n&&(s["start-date"]=a(c),s["end-date"]=a(r),s["eligibility-indicator"]="true");return s}const r={transactionType:"ALL",filterTranType:"ALL",statementPeriodId:"ALL",downloadType:"CSV",accountId:t.id};if("all"===n&&(r.dateOption="LAST_24_MONTHS"),"date-range"===n){if(!e.from||!e.to)throw new Error("date-range requires from and to in mm/dd/yyyy format");r.dateOption="DATE_RANGE",r.dateLo=e.from,r.dateHi=e.to}return r}export async function fetchAccountCsv(t,n,a,o){const c=function(t){const n=e[t.summaryType];if(!n)throw new Error(`Unsupported account type for ${formatAccountLabel(t)}: ${t.summaryType}`);return n}(n),s={body:r(n,a,o),countUrl:c.count,csvUrl:c.csv,csrfUrl:"/svc/rl/accounts/secure/v1/csrf/token/list",mode:c.mode,headers:{"content-type":"application/x-www-form-urlencoded; charset=UTF-8","x-jpmc-channel":"id=C30","x-jpmc-csrf-token":"NONE"}},i=await t.evaluate(async t=>{const e=t=>new URLSearchParams(t).toString(),n=(n,a)=>fetch(n+"?"+e(a),{method:"GET",credentials:"include",headers:t.headers}),a=(n,a,o=t.headers)=>fetch(n,{method:"POST",credentials:"include",headers:o,body:e(a)}),o="cardGet"===t.mode?await n(t.countUrl,t.body):await a(t.countUrl,t.body);if(!o.ok)throw new Error("Download count request failed with status "+o.status);const r=await fetch(t.csrfUrl,{method:"POST",credentials:"include",headers:t.headers,body:""});if(!r.ok)throw new Error("CSRF token request failed with status "+r.status);const c=await r.json(),s=c.csrfToken||c.response?.csrfToken;if(!s)throw new Error("CSRF token was not present in Chase token response");const i={...t.body,csrftoken:s,submit:"Submit"},d="cardGet"===t.mode?await n(t.csvUrl,i):await a(t.csvUrl,i,{"content-type":"application/x-www-form-urlencoded"});return{status:d.status,contentType:d.headers.get("content-type")||"",text:await d.text()}},s);if(i.status<200||i.status>=300)throw new Error(`Download request failed with status ${i.status} for ${formatAccountLabel(n)}`);if(i.contentType&&!/csv|text|octet-stream/i.test(i.contentType))throw new Error(`Download for ${formatAccountLabel(n)} returned ${i.contentType||"unknown content type"}`);return i.text}export async function downloadAccounts(e,n){const a=n.options,o=normalizeActivity(a.activity||a.range);if(!("date-range"!==o.key||a.from&&a.to))throw new Error("activity date-range requires from and to in mm/dd/yyyy format");const r=collectAccounts(await fetchDownloadOptions(e));if(!r.length)throw new Error("No downloadable accounts were found in the Chase download options response.");if(a.list)return summarizeAccounts(r);const c=selectAccounts(r,a),s=[];for(const[r,i]of c.entries()){const d=formatAccountLabel(i),u=t(await fetchAccountCsv(e,i,a,o.key),d);if(a.download){const t=a.filename&&1===c.length?a.filename:accountFileName(i),e=await n.save(t,u);s.push(`Saved: ${e}`)}else s.push(`\n===== ${r+1}/${c.length}: ${d} =====\n`+(u.endsWith("\n")?u:`${u}\n`))}return s.join("\n")}
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../types.js").Site;
2
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as e}from"../../core/define-site.js";import{downloadAccounts as t}from"./download-helper.js";export default e({id:"chase",name:"Chase Bank",domain:"chase.com",description:"Logs into Chase, lists downloadable accounts, and downloads statement/transaction CSV files.",transport:"browser",cookies:"optional",keepBrowserOpen:!0,auth:{intendedUrl:"https://secure.chase.com/web/auth/dashboard#/dashboard/overview",emailSelector:'input[name="username"]',passwordSelector:'input[name="password"]',submitButtonSelector:"#signin-button",delayMs:1e3,pwdSelector:'input[type="password"], input[name="password"], input[id*="password"]',usernameSelectors:["#userId-input-field-input","input#userId-input","input#userId",'input[name="usr_name"]'],passwordSelectors:["#password-input-field-input","input#password-input","input#password"],submitSelectors:["#signin-button",'button[type="submit"]'],dashboardSelectors:[".accounts-group-container-bc","#account-groups-component-bc",'[data-testid="accounts-group-container"]',".innerTile","#DDA_ACCOUNTS"]},positionals:[{name:"accounts",description:"Account indexes to select (e.g. 1 3). Leave empty for all.",required:!1,variadic:!0}],parameters:[{name:"list",type:"boolean",description:"List downloadable accounts only",short:"l",default:!1},{name:"download",type:"boolean",description:"Save selected account CSV file(s) to cwd or --out-dir",short:"d",default:!1},{name:"limit",type:"number",description:"Only process the first n accounts"},{name:"first",type:"boolean",description:"Shortcut for --limit 1",default:!1},{name:"filename",type:"string",description:"Save one selected account to this filename (requires --download and one account)"},{name:"activity",type:"string",description:"Activity: current-display, all-transactions, date-range (default: all)"},{name:"range",type:"string",description:"Alias for --activity"},{name:"from",type:"string",description:"Start date for date-range (mm/dd/yyyy)"},{name:"to",type:"string",description:"End date for date-range (mm/dd/yyyy)"},{name:"out-dir",type:"string",description:"Write each CSV to <dir> instead of cwd"}],run:async e=>{const n=await e.browser();return await n.waitForTimeout(3e3),e.debug&&console.log("[chase] Running statement downloader flow..."),t(n,e)}});
@@ -0,0 +1,10 @@
1
+ /**
2
+ * ChatGPT / Codex usage. A two-step flow:
3
+ * 1. Exchange Chrome cookies for a Bearer JWT via the Next-Auth session endpoint.
4
+ * 2. Use the JWT to query the private wham/usage endpoint.
5
+ *
6
+ * Cookie + User-Agent injection is handled by ctx.http, so the site only
7
+ * describes the two requests.
8
+ */
9
+ declare const _default: import("../../types.js").Site;
10
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as t}from"../../core/define-site.js";export default t({id:"codex-usage",name:"ChatGPT / Codex Usage",domain:"chatgpt.com",description:"Fetches ChatGPT rate limit usage and quota details from the private wham/usage API.",run:async t=>{const a=await t.http.json("https://chatgpt.com/api/auth/session");if(!a?.accessToken)throw new Error("No ChatGPT login found in browser");return t.http.json("https://chatgpt.com/backend-api/wham/usage",{headers:{authorization:`Bearer ${a.accessToken}`}})}});
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Cursor.com — active usage summary. Pure declarative single-endpoint site;
3
+ * the runtime injects cookies + User-Agent and parses JSON.
4
+ */
5
+ declare const _default: import("../../types.js").Site;
6
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as r}from"../../core/define-site.js";export default r({id:"cursor-usage",name:"Cursor Usage",domain:"cursor.com",description:"Fetches the active Cursor usage summary from the private usage-summary API.",endpoints:[{url:"https://cursor.com/api/usage-summary"}]});
@@ -0,0 +1,5 @@
1
+ import { type UniversalGoogleSchema } from "../../util/google-json.js";
2
+ /** Schema mapping Gemini's batchexecute response indices to a usage summary. */
3
+ export declare const GEMINI_USAGE_SCHEMA: UniversalGoogleSchema;
4
+ declare const _default: import("../../types.js").Site;
5
+ export default _default;
@@ -0,0 +1 @@
1
+ import{defineSite as e}from"../../core/define-site.js";import{decodeGoogleJsonWithSchema as t}from"../../util/google-json.js";export const GEMINI_USAGE_SCHEMA={planCode:{path:[0]},planName:{path:[0],transform:e=>2===e?"Gemini Pro":1===e?"Gemini Free":`Unknown (${e})`},limits:{path:[1],items:{rawLimit:{path:[0]},percentageUsed:{path:[1],transform:e=>parseFloat((100*e).toFixed(2))},tierName:{path:[2],transform:e=>1===e?"Hourly Usage Limit":2===e?"Weekly Usage Limit":`Unknown Tier (${e})`},resetTime:{path:[3,0,0],transform:e=>e?new Date(1e3*e).toISOString():null}}}};export default e({id:"gemini-usage",name:"Gemini Usage",domain:"gemini.google.com",description:"Fetches Gemini account usage/quota details via browser-attached Playwright.",transport:"browser",cookies:"optional",endpoints:[{url:"https://gemini.google.com/usage"}],run:async e=>{const o=await e.browser(),i=new Promise((e,t)=>{const i=setTimeout(()=>t(new Error("Timeout waiting for Gemini usage RPC payload. Make sure you are logged into gemini.google.com in Chrome.")),15e3);o.on("response",async t=>{if(t.url().includes("jSf9Qc"))try{const o=await t.text();clearTimeout(i),e(o)}catch{}})});e.debug&&console.log("Reloading to capture Gemini usage network request..."),await o.reload({waitUntil:"domcontentloaded"});const a=await i;return t(a,"jSf9Qc",GEMINI_USAGE_SCHEMA)}});
@@ -0,0 +1,24 @@
1
+ export declare function formEncode(value: any): string;
2
+ export declare function utf8Bytes(codePoint: number): number[];
3
+ export declare function utf8String(bytes: number[]): string;
4
+ export declare function percentDecode(value: any): string;
5
+ export declare function parseQueryString(search: string): Record<string, string>;
6
+ export declare function buildQueryString(query: Record<string, any>): string;
7
+ export declare function googlePath(url: string): string | null;
8
+ export declare function stripXssi(text: string): string;
9
+ export declare function parseJsonMaybe(text: string): any;
10
+ export declare function parseGoogleRecordStream(text: string): any[];
11
+ export declare function decodeGoogleBody({ body, contentType }: {
12
+ body: string;
13
+ contentType?: string;
14
+ }): {
15
+ format: string;
16
+ xssiPrefixed: boolean;
17
+ parsed: any;
18
+ records: any[];
19
+ };
20
+ export declare function cleanText(s: any, limit?: number): string;
21
+ export declare function extractAnswerFromText(text: string): string | null;
22
+ export declare function cleanHtml(html: string): string;
23
+ export declare function findHtmlInObject(obj: any): string | null;
24
+ export declare function extractAnswerFromRecordStream(text: string): string | null;