website-api 1.1.2 → 1.1.4

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 (68) hide show
  1. package/README.md +141 -1
  2. package/dist/bin/cli.js +204 -1
  3. package/dist/src/capabilities/browser.d.ts +8 -2
  4. package/dist/src/capabilities/browser.js +106 -1
  5. package/dist/src/capabilities/cookies.d.ts +7 -1
  6. package/dist/src/capabilities/cookies.js +68 -1
  7. package/dist/src/capabilities/download.js +32 -1
  8. package/dist/src/capabilities/fingerprint.js +62 -1
  9. package/dist/src/capabilities/http.js +101 -1
  10. package/dist/src/capabilities/login/login-helper.js +185 -1
  11. package/dist/src/capabilities/login/login-strategy.js +36 -1
  12. package/dist/src/challenges/perimeterx.d.ts +62 -0
  13. package/dist/src/challenges/perimeterx.js +112 -0
  14. package/dist/src/cli/ext.js +338 -1
  15. package/dist/src/core/context.d.ts +2 -2
  16. package/dist/src/core/context.js +137 -1
  17. package/dist/src/core/define-site.js +74 -1
  18. package/dist/src/core/loader.js +142 -1
  19. package/dist/src/core/registry.js +332 -1
  20. package/dist/src/core/runtime.d.ts +12 -4
  21. package/dist/src/core/runtime.js +98 -1
  22. package/dist/src/env.js +34 -1
  23. package/dist/src/sites/bloomberg.com/index.d.ts +11 -0
  24. package/dist/src/sites/bloomberg.com/index.js +49 -0
  25. package/dist/src/sites/bloomberg.com/openapi.yaml +38 -0
  26. package/dist/src/sites/chase.com/download-helper.js +266 -1
  27. package/dist/src/sites/chase.com/index.js +87 -1
  28. package/dist/src/sites/chase.com/openapi.yaml +76 -0
  29. package/dist/src/sites/chatgpt.com/index.js +24 -1
  30. package/dist/src/sites/chatgpt.com/openapi.yaml +29 -0
  31. package/dist/src/sites/claude.ai/claude-helpers.d.ts +20 -0
  32. package/dist/src/sites/claude.ai/claude-helpers.js +26 -0
  33. package/dist/src/sites/claude.ai/index.d.ts +2 -0
  34. package/dist/src/sites/claude.ai/index.js +42 -0
  35. package/dist/src/sites/claude.ai/openapi.yaml +54 -0
  36. package/dist/src/sites/cursor.com/index.js +12 -1
  37. package/dist/src/sites/cursor.com/openapi.yaml +39 -0
  38. package/dist/src/sites/e-zpassny.com/index.d.ts +2 -0
  39. package/dist/src/sites/e-zpassny.com/index.js +344 -0
  40. package/dist/src/sites/e-zpassny.com/openapi.yaml +68 -0
  41. package/dist/src/sites/gemini.google.com/index.d.ts +11 -0
  42. package/dist/src/sites/gemini.google.com/index.js +80 -1
  43. package/dist/src/sites/gemini.google.com/openapi.yaml +39 -0
  44. package/dist/src/sites/google.com/google-helpers.js +255 -1
  45. package/dist/src/sites/google.com/index.js +253 -1
  46. package/dist/src/sites/google.com/openapi.yaml +59 -0
  47. package/dist/src/sites/ollama.com/index.js +43 -1
  48. package/dist/src/sites/ollama.com/openapi.yaml +39 -0
  49. package/dist/src/sites/perplexity.ai/index.js +253 -1
  50. package/dist/src/sites/perplexity.ai/openapi.yaml +51 -0
  51. package/dist/src/sites/pseg.com/index.js +243 -1
  52. package/dist/src/sites/pseg.com/openapi.yaml +42 -0
  53. package/dist/src/sites/pseg.com/pseg-helpers.js +53 -1
  54. package/dist/src/sites/voice.google.com/index.d.ts +2 -0
  55. package/dist/src/sites/voice.google.com/index.js +122 -0
  56. package/dist/src/sites/voice.google.com/openapi.yaml +67 -0
  57. package/dist/src/sites/voice.google.com/voice-helpers.d.ts +105 -0
  58. package/dist/src/sites/voice.google.com/voice-helpers.js +181 -0
  59. package/dist/src/sites/zillow.com/index.d.ts +2 -0
  60. package/dist/src/sites/zillow.com/index.js +303 -0
  61. package/dist/src/sites/zillow.com/openapi.yaml +55 -0
  62. package/dist/src/types.d.ts +16 -0
  63. package/dist/src/types.js +1 -1
  64. package/dist/src/util/args-parser.js +145 -1
  65. package/dist/src/util/google-json.js +74 -1
  66. package/dist/src/website-api.d.ts +7 -7
  67. package/dist/src/website-api.js +13 -1
  68. package/package.json +37 -10
@@ -0,0 +1,49 @@
1
+ import { solvePerimeterX } from "../../challenges/perimeterx.js";
2
+ import { defineSite } from "../../core/define-site.js";
3
+ const BILLIONAIRES_URL = "https://www.bloomberg.com/billionaires/";
4
+ /**
5
+ * The page embeds the entire ranking inline as a global:
6
+ * `window.top500 = [ ...500 objects... ];`
7
+ * Each entry carries rank, worth/fWorth, biography, netWorthSummary, milestones,
8
+ * public/private/cash assets, schools, industry, etc. There is no JSON API — the
9
+ * data lives in the HTML — and the site is fronted by PerimeterX, so a plain
10
+ * fetch gets flagged. We drive the CDP-attached Chrome, clear any PerimeterX
11
+ * challenge automatically, then read the global out of the page.
12
+ */
13
+ export default defineSite({
14
+ id: "bloomberg-billionaires",
15
+ name: "Bloomberg Billionaires Index",
16
+ domain: "bloomberg.com",
17
+ description: "Extracts the full Bloomberg Billionaires Index (window.top500) via the browser, auto-solving the PerimeterX challenge.",
18
+ transport: "browser",
19
+ cookies: "optional", // public data — no login required
20
+ // `--out <file>` and `--text` are global CLI flags; the framework writes
21
+ // whatever we return, so we just hand back the ranking array.
22
+ parameters: [
23
+ { name: "top", type: "number", description: "Only return the first N entries" },
24
+ { name: "hold", type: "number", description: "PerimeterX press-&-hold duration in ms (default 11000)" },
25
+ ],
26
+ run: async (ctx) => {
27
+ const page = await ctx.browser();
28
+ const log = (m) => ctx.debug && console.log(m);
29
+ // Ensure we're on the billionaires path even if an existing bloomberg tab was reused.
30
+ if (!page.url().startsWith(BILLIONAIRES_URL)) {
31
+ await page.goto(BILLIONAIRES_URL, { waitUntil: "domcontentloaded" });
32
+ }
33
+ const holdMs = ctx.options.hold !== undefined ? Number(ctx.options.hold) : undefined;
34
+ const challenge = await solvePerimeterX(page, { holdMs, log });
35
+ if (!challenge.cleared) {
36
+ const ref = challenge.referenceId ? ` (reference ${challenge.referenceId})` : "";
37
+ throw new Error(challenge.kind === "hard-block"
38
+ ? `Bloomberg served a PerimeterX hard block${ref}. The IP reputation is flagged — wait before retrying.`
39
+ : `Could not clear the PerimeterX press-&-hold challenge${ref}.`);
40
+ }
41
+ // window.top500 is set by an inline script; wait for it to populate.
42
+ await page.waitForFunction(() => Array.isArray(window.top500) && window.top500.length > 0, { timeout: 20_000 });
43
+ let data = await page.evaluate(() => window.top500);
44
+ const top = ctx.options.top !== undefined ? Number(ctx.options.top) : undefined;
45
+ if (top && top > 0)
46
+ data = data.slice(0, top);
47
+ return data;
48
+ },
49
+ });
@@ -0,0 +1,38 @@
1
+ # Generated by `pnpm generate:openapi` — do not edit by hand.
2
+ openapi: 3.1.0
3
+ info:
4
+ title: Bloomberg Billionaires Index
5
+ description: Extracts the full Bloomberg Billionaires Index (window.top500) via the browser,
6
+ auto-solving the PerimeterX challenge.
7
+ version: 1.1.3
8
+ servers:
9
+ - url: https://bloomberg.com
10
+ paths: {}
11
+ components:
12
+ securitySchemes:
13
+ chromeSession:
14
+ type: apiKey
15
+ in: cookie
16
+ name: session
17
+ description: "Authenticated via the user's real Chrome session: website-api injects decrypted Chrome
18
+ cookies for bloomberg.com into every request."
19
+ x-website-api:
20
+ id: bloomberg-billionaires
21
+ domain: bloomberg.com
22
+ cookieDomain: bloomberg.com
23
+ transport: browser
24
+ cookies: optional
25
+ requiresLogin: false
26
+ imperative: true
27
+ cli:
28
+ command: website-api bloomberg-billionaires
29
+ positionals: []
30
+ parameters:
31
+ - flag: --top
32
+ type: number
33
+ description: Only return the first N entries
34
+ required: false
35
+ - flag: --hold
36
+ type: number
37
+ description: PerimeterX press-&-hold duration in ms (default 11000)
38
+ required: false
@@ -1 +1,266 @@
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")}
1
+ import { assertNotHtml } from "../../capabilities/download.js";
2
+ const CHASE_DASHBOARD_URL = "https://secure.chase.com/web/auth/dashboard#/dashboard/overview";
3
+ const DOWNLOAD_OPTIONS_URL = "/svc/rr/accounts/secure/v1/account/activity/download/options/list";
4
+ const CSRF_TOKEN_URL = "/svc/rl/accounts/secure/v1/csrf/token/list";
5
+ const DOWNLOAD_ENDPOINTS = {
6
+ CARD: {
7
+ mode: "cardGet",
8
+ count: "/svc/rr/accounts/secure/gateway/credit-card/transactions/inquiry-maintenance/digital-transaction-activity/v1/transaction-counts",
9
+ csv: "/svc/rr/accounts/secure/gateway/credit-card/transactions/inquiry-maintenance/digital-transaction-activity/v1/transaction-activities",
10
+ },
11
+ DDA: {
12
+ mode: "formPost",
13
+ count: "/svc/rr/accounts/secure/v1/account/activity/download/count/dda/list",
14
+ csv: "/svc/rr/accounts/secure/v1/account/activity/download/dda/list",
15
+ },
16
+ };
17
+ const ACTIVITY_OPTIONS = new Map([
18
+ ["current", { key: "current", label: "Current display, including filters" }],
19
+ ["current-display", { key: "current", label: "Current display, including filters" }],
20
+ ["all", { key: "all", label: "All transactions" }],
21
+ ["all-transactions", { key: "all", label: "All transactions" }],
22
+ ["date-range", { key: "date-range", label: "Choose a date range" }],
23
+ ["date range", { key: "date-range", label: "Choose a date range" }],
24
+ ["choose-date-range", { key: "date-range", label: "Choose a date range" }],
25
+ ]);
26
+ const DEFAULT_ACTIVITY = "all";
27
+ export function normalizeActivity(value) {
28
+ const key = String(value || "")
29
+ .trim()
30
+ .toLowerCase();
31
+ const option = ACTIVITY_OPTIONS.get(key || DEFAULT_ACTIVITY);
32
+ if (!option)
33
+ throw new Error(`Unknown activity option: ${value}`);
34
+ return option;
35
+ }
36
+ function requestHeaders() {
37
+ return {
38
+ "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
39
+ "x-jpmc-channel": "id=C30",
40
+ "x-jpmc-csrf-token": "NONE",
41
+ };
42
+ }
43
+ export async function fetchDownloadOptions(page) {
44
+ if (!page.url().includes("secure.chase.com")) {
45
+ await page.goto(CHASE_DASHBOARD_URL, { waitUntil: "domcontentloaded" });
46
+ }
47
+ const result = await page.evaluate(async (input) => {
48
+ const response = await fetch(input.url, {
49
+ method: "POST",
50
+ credentials: "include",
51
+ headers: input.headers,
52
+ body: "",
53
+ });
54
+ return { status: response.status, text: await response.text() };
55
+ }, { url: DOWNLOAD_OPTIONS_URL, headers: requestHeaders() });
56
+ if (result.status < 200 || result.status >= 300) {
57
+ throw new Error("Download options request failed with status " + result.status);
58
+ }
59
+ return JSON.parse(result.text);
60
+ }
61
+ export function collectAccounts(payload) {
62
+ return (payload.downloadAccountActivityOptions || [])
63
+ .map((account) => ({
64
+ id: String(account.accountId || "").trim(),
65
+ summaryType: String(account.summaryType || "").trim(),
66
+ detailType: String(account.detailType || "").trim(),
67
+ nickname: String(account.nickName || "").trim(),
68
+ mask: String(account.mask || "").trim(),
69
+ }))
70
+ .filter((a) => a.id && (a.summaryType === "CARD" || a.summaryType === "DDA") && a.detailType);
71
+ }
72
+ export function formatAccountLabel(account) {
73
+ const bits = [];
74
+ if (account.nickname)
75
+ bits.push(account.nickname);
76
+ if (account.mask)
77
+ bits.push(`****${account.mask}`);
78
+ if (!bits.length)
79
+ bits.push(`${account.summaryType}/${account.detailType}/${account.id}`);
80
+ return bits.join(" ");
81
+ }
82
+ export function accountFileName(account) {
83
+ const base = [account.nickname || "account", account.summaryType, account.detailType, account.id]
84
+ .map((part) => String(part)
85
+ .trim()
86
+ .replace(/[^A-Za-z0-9._-]+/g, "-"))
87
+ .filter(Boolean)
88
+ .join("-")
89
+ .replace(/-+/g, "-")
90
+ .replace(/^-|-$/g, "");
91
+ return `${base || "account"}.csv`;
92
+ }
93
+ export function summarizeAccounts(accounts) {
94
+ const lines = [`Found ${accounts.length} downloadable account${accounts.length === 1 ? "" : "s"}:`];
95
+ for (const [index, account] of accounts.entries()) {
96
+ lines.push(`${index + 1}. ${formatAccountLabel(account)} | ${account.summaryType},${account.detailType},${account.id}`);
97
+ }
98
+ return lines.join("\n");
99
+ }
100
+ export function selectAccounts(accounts, opts) {
101
+ let indexes = [];
102
+ if (opts.accounts)
103
+ indexes = opts.accounts
104
+ .split(/[\s,]+/)
105
+ .map(Number)
106
+ .filter((n) => !Number.isNaN(n));
107
+ if (!indexes.length) {
108
+ const limitVal = opts.limit !== undefined ? opts.limit : opts.first ? 1 : null;
109
+ return accounts.slice(0, limitVal || undefined);
110
+ }
111
+ const selected = [];
112
+ const seen = new Set();
113
+ for (const n of indexes) {
114
+ if (n < 1 || n > accounts.length) {
115
+ throw new Error(`Account number ${n} is out of range. Run list to see 1-${accounts.length}.`);
116
+ }
117
+ if (seen.has(n))
118
+ continue;
119
+ seen.add(n);
120
+ selected.push(accounts[n - 1]);
121
+ }
122
+ return selected;
123
+ }
124
+ function getEndpoints(account) {
125
+ const endpoints = DOWNLOAD_ENDPOINTS[account.summaryType];
126
+ if (!endpoints)
127
+ throw new Error(`Unsupported account type for ${formatAccountLabel(account)}: ${account.summaryType}`);
128
+ return endpoints;
129
+ }
130
+ function formatYmd(date) {
131
+ return `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, "0")}${String(date.getDate()).padStart(2, "0")}`;
132
+ }
133
+ function parseDateOption(value) {
134
+ const match = String(value || "").match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
135
+ if (!match)
136
+ throw new Error(`Invalid date "${value}". Use mm/dd/yyyy format.`);
137
+ const [, month, day, year] = match;
138
+ return `${year}${month.padStart(2, "0")}${day.padStart(2, "0")}`;
139
+ }
140
+ function buildDownloadBody(account, opts, activityKey) {
141
+ if (account.summaryType === "CARD") {
142
+ const today = new Date();
143
+ const twoYearsAgo = new Date(today);
144
+ twoYearsAgo.setFullYear(today.getFullYear() - 2);
145
+ const body = {
146
+ "account-activity-download-type-code": "CSV",
147
+ "digital-account-identifier": account.id,
148
+ };
149
+ if (activityKey === "date-range") {
150
+ if (!opts.from || !opts.to)
151
+ throw new Error("date-range requires from and to in mm/dd/yyyy format");
152
+ body["start-date"] = parseDateOption(opts.from);
153
+ body["end-date"] = parseDateOption(opts.to);
154
+ }
155
+ else if (activityKey === "all") {
156
+ body["start-date"] = formatYmd(twoYearsAgo);
157
+ body["end-date"] = formatYmd(today);
158
+ body["eligibility-indicator"] = "true";
159
+ }
160
+ return body;
161
+ }
162
+ const body = {
163
+ transactionType: "ALL",
164
+ filterTranType: "ALL",
165
+ statementPeriodId: "ALL",
166
+ downloadType: "CSV",
167
+ accountId: account.id,
168
+ };
169
+ if (activityKey === "all")
170
+ body.dateOption = "LAST_24_MONTHS";
171
+ if (activityKey === "date-range") {
172
+ if (!opts.from || !opts.to)
173
+ throw new Error("date-range requires from and to in mm/dd/yyyy format");
174
+ body.dateOption = "DATE_RANGE";
175
+ body.dateLo = opts.from;
176
+ body.dateHi = opts.to;
177
+ }
178
+ return body;
179
+ }
180
+ export async function fetchAccountCsv(page, account, opts, activityKey) {
181
+ const endpoints = getEndpoints(account);
182
+ const input = {
183
+ body: buildDownloadBody(account, opts, activityKey),
184
+ countUrl: endpoints.count,
185
+ csvUrl: endpoints.csv,
186
+ csrfUrl: CSRF_TOKEN_URL,
187
+ mode: endpoints.mode,
188
+ headers: requestHeaders(),
189
+ };
190
+ const result = await page.evaluate(async (input) => {
191
+ const encode = (data) => new URLSearchParams(data).toString();
192
+ const getWithQuery = (url, data) => fetch(url + "?" + encode(data), {
193
+ method: "GET",
194
+ credentials: "include",
195
+ headers: input.headers,
196
+ });
197
+ const postForm = (url, data, headers = input.headers) => fetch(url, {
198
+ method: "POST",
199
+ credentials: "include",
200
+ headers: headers,
201
+ body: encode(data),
202
+ });
203
+ const countResponse = input.mode === "cardGet"
204
+ ? await getWithQuery(input.countUrl, input.body)
205
+ : await postForm(input.countUrl, input.body);
206
+ if (!countResponse.ok)
207
+ throw new Error("Download count request failed with status " + countResponse.status);
208
+ const tokenResponse = await fetch(input.csrfUrl, {
209
+ method: "POST",
210
+ credentials: "include",
211
+ headers: input.headers,
212
+ body: "",
213
+ });
214
+ if (!tokenResponse.ok)
215
+ throw new Error("CSRF token request failed with status " + tokenResponse.status);
216
+ const tokenPayload = await tokenResponse.json();
217
+ const csrfToken = tokenPayload.csrfToken || tokenPayload.response?.csrfToken;
218
+ if (!csrfToken)
219
+ throw new Error("CSRF token was not present in Chase token response");
220
+ const csvData = { ...input.body, csrftoken: csrfToken, submit: "Submit" };
221
+ const csvResponse = input.mode === "cardGet"
222
+ ? await getWithQuery(input.csvUrl, csvData)
223
+ : await postForm(input.csvUrl, csvData, { "content-type": "application/x-www-form-urlencoded" });
224
+ return {
225
+ status: csvResponse.status,
226
+ contentType: csvResponse.headers.get("content-type") || "",
227
+ text: await csvResponse.text(),
228
+ };
229
+ }, input);
230
+ if (result.status < 200 || result.status >= 300) {
231
+ throw new Error(`Download request failed with status ${result.status} for ${formatAccountLabel(account)}`);
232
+ }
233
+ if (result.contentType && !/csv|text|octet-stream/i.test(result.contentType)) {
234
+ throw new Error(`Download for ${formatAccountLabel(account)} returned ${result.contentType || "unknown content type"}`);
235
+ }
236
+ return result.text;
237
+ }
238
+ /** Discovers, selects, downloads, and saves (or prints) Chase account CSVs. */
239
+ export async function downloadAccounts(page, ctx) {
240
+ const opts = ctx.options;
241
+ const activityOption = normalizeActivity(opts.activity || opts.range);
242
+ if (activityOption.key === "date-range" && (!opts.from || !opts.to)) {
243
+ throw new Error("activity date-range requires from and to in mm/dd/yyyy format");
244
+ }
245
+ const allAccounts = collectAccounts(await fetchDownloadOptions(page));
246
+ if (!allAccounts.length)
247
+ throw new Error("No downloadable accounts were found in the Chase download options response.");
248
+ if (opts.list)
249
+ return summarizeAccounts(allAccounts);
250
+ const accountsToProcess = selectAccounts(allAccounts, opts);
251
+ const results = [];
252
+ for (const [index, account] of accountsToProcess.entries()) {
253
+ const label = formatAccountLabel(account);
254
+ const csvText = assertNotHtml(await fetchAccountCsv(page, account, opts, activityOption.key), label);
255
+ if (opts.download) {
256
+ const fileName = opts.filename && accountsToProcess.length === 1 ? opts.filename : accountFileName(account);
257
+ const savedPath = await ctx.save(fileName, csvText);
258
+ results.push(`Saved: ${savedPath}`);
259
+ }
260
+ else {
261
+ results.push(`\n===== ${index + 1}/${accountsToProcess.length}: ${label} =====\n` +
262
+ (csvText.endsWith("\n") ? csvText : `${csvText}\n`));
263
+ }
264
+ }
265
+ return results.join("\n");
266
+ }
@@ -1 +1,87 @@
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)}});
1
+ import { defineSite } from "../../core/define-site.js";
2
+ import { downloadAccounts } from "./download-helper.js";
3
+ export default defineSite({
4
+ id: "chase",
5
+ name: "Chase Bank",
6
+ domain: "chase.com",
7
+ description: "Logs into Chase, lists downloadable accounts, and downloads statement/transaction CSV files.",
8
+ transport: "browser",
9
+ cookies: "optional",
10
+ // Keep the authenticated tab open so the Chase session (and any 2FA) survives
11
+ // for the next command instead of forcing a fresh login each run.
12
+ keepBrowserOpen: true,
13
+ // Declarative login — connect() auto-detects the session and logs in only when
14
+ // needed. The logon form renders inside a secure.chase.com subframe; the
15
+ // helper fills whichever candidate is visible there. Primary selectors target
16
+ // the stable name="" attributes; id-based fallbacks cover DOM variants.
17
+ auth: {
18
+ intendedUrl: "https://secure.chase.com/web/auth/dashboard#/dashboard/overview",
19
+ emailSelector: 'input[name="username"]',
20
+ passwordSelector: 'input[name="password"]',
21
+ submitButtonSelector: "#signin-button",
22
+ delayMs: 1000,
23
+ pwdSelector: 'input[type="password"], input[name="password"], input[id*="password"]',
24
+ usernameSelectors: [
25
+ "#userId-input-field-input",
26
+ "input#userId-input",
27
+ "input#userId",
28
+ 'input[name="usr_name"]',
29
+ ],
30
+ passwordSelectors: ["#password-input-field-input", "input#password-input", "input#password"],
31
+ submitSelectors: ["#signin-button", 'button[type="submit"]'],
32
+ dashboardSelectors: [
33
+ ".accounts-group-container-bc",
34
+ "#account-groups-component-bc",
35
+ '[data-testid="accounts-group-container"]',
36
+ ".innerTile",
37
+ "#DDA_ACCOUNTS",
38
+ ],
39
+ },
40
+ positionals: [
41
+ {
42
+ name: "accounts",
43
+ description: "Account indexes to select (e.g. 1 3). Leave empty for all.",
44
+ required: false,
45
+ variadic: true,
46
+ },
47
+ ],
48
+ parameters: [
49
+ {
50
+ name: "list",
51
+ type: "boolean",
52
+ description: "List downloadable accounts only",
53
+ short: "l",
54
+ default: false,
55
+ },
56
+ {
57
+ name: "download",
58
+ type: "boolean",
59
+ description: "Save selected account CSV file(s) to cwd or --out-dir",
60
+ short: "d",
61
+ default: false,
62
+ },
63
+ { name: "limit", type: "number", description: "Only process the first n accounts" },
64
+ { name: "first", type: "boolean", description: "Shortcut for --limit 1", default: false },
65
+ {
66
+ name: "filename",
67
+ type: "string",
68
+ description: "Save one selected account to this filename (requires --download and one account)",
69
+ },
70
+ {
71
+ name: "activity",
72
+ type: "string",
73
+ description: "Activity: current-display, all-transactions, date-range (default: all)",
74
+ },
75
+ { name: "range", type: "string", description: "Alias for --activity" },
76
+ { name: "from", type: "string", description: "Start date for date-range (mm/dd/yyyy)" },
77
+ { name: "to", type: "string", description: "End date for date-range (mm/dd/yyyy)" },
78
+ { name: "out-dir", type: "string", description: "Write each CSV to <dir> instead of cwd" },
79
+ ],
80
+ run: async (ctx) => {
81
+ const page = await ctx.browser();
82
+ await page.waitForTimeout(3000); // let the dashboard settle after navigation/login
83
+ if (ctx.debug)
84
+ console.log("[chase] Running statement downloader flow...");
85
+ return downloadAccounts(page, ctx);
86
+ },
87
+ });
@@ -0,0 +1,76 @@
1
+ # Generated by `pnpm generate:openapi` — do not edit by hand.
2
+ openapi: 3.1.0
3
+ info:
4
+ title: Chase Bank
5
+ description: Logs into Chase, lists downloadable accounts, and downloads statement/transaction CSV files.
6
+ version: 1.1.3
7
+ servers:
8
+ - url: https://chase.com
9
+ paths: {}
10
+ components:
11
+ securitySchemes:
12
+ chromeSession:
13
+ type: apiKey
14
+ in: cookie
15
+ name: session
16
+ description: "Authenticated via the user's real Chrome session: website-api injects decrypted Chrome
17
+ cookies for chase.com into every request."
18
+ x-website-api:
19
+ id: chase
20
+ domain: chase.com
21
+ cookieDomain: chase.com
22
+ transport: browser
23
+ cookies: optional
24
+ requiresLogin: true
25
+ imperative: true
26
+ cli:
27
+ command: website-api chase
28
+ positionals:
29
+ - name: accounts
30
+ description: Account indexes to select (e.g. 1 3). Leave empty for all.
31
+ required: false
32
+ variadic: true
33
+ parameters:
34
+ - flag: --list
35
+ type: boolean
36
+ description: List downloadable accounts only
37
+ default: false
38
+ required: false
39
+ - flag: --download
40
+ type: boolean
41
+ description: Save selected account CSV file(s) to cwd or --out-dir
42
+ default: false
43
+ required: false
44
+ - flag: --limit
45
+ type: number
46
+ description: Only process the first n accounts
47
+ required: false
48
+ - flag: --first
49
+ type: boolean
50
+ description: Shortcut for --limit 1
51
+ default: false
52
+ required: false
53
+ - flag: --filename
54
+ type: string
55
+ description: Save one selected account to this filename (requires --download and one account)
56
+ required: false
57
+ - flag: --activity
58
+ type: string
59
+ description: "Activity: current-display, all-transactions, date-range (default: all)"
60
+ required: false
61
+ - flag: --range
62
+ type: string
63
+ description: Alias for --activity
64
+ required: false
65
+ - flag: --from
66
+ type: string
67
+ description: Start date for date-range (mm/dd/yyyy)
68
+ required: false
69
+ - flag: --to
70
+ type: string
71
+ description: End date for date-range (mm/dd/yyyy)
72
+ required: false
73
+ - flag: --out-dir
74
+ type: string
75
+ description: Write each CSV to <dir> instead of cwd
76
+ required: false
@@ -1 +1,24 @@
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}`}})}});
1
+ import { defineSite } from "../../core/define-site.js";
2
+ /**
3
+ * ChatGPT / Codex usage. A two-step flow:
4
+ * 1. Exchange Chrome cookies for a Bearer JWT via the Next-Auth session endpoint.
5
+ * 2. Use the JWT to query the private wham/usage endpoint.
6
+ *
7
+ * Cookie + User-Agent injection is handled by ctx.http, so the site only
8
+ * describes the two requests.
9
+ */
10
+ export default defineSite({
11
+ id: "codex-usage",
12
+ name: "ChatGPT / Codex Usage",
13
+ domain: "chatgpt.com",
14
+ description: "Fetches ChatGPT rate limit usage and quota details from the private wham/usage API.",
15
+ run: async (ctx) => {
16
+ const session = await ctx.http.json("https://chatgpt.com/api/auth/session");
17
+ if (!session?.accessToken) {
18
+ throw new Error("No ChatGPT login found in browser");
19
+ }
20
+ return ctx.http.json("https://chatgpt.com/backend-api/wham/usage", {
21
+ headers: { authorization: `Bearer ${session.accessToken}` },
22
+ });
23
+ },
24
+ });
@@ -0,0 +1,29 @@
1
+ # Generated by `pnpm generate:openapi` — do not edit by hand.
2
+ openapi: 3.1.0
3
+ info:
4
+ title: ChatGPT / Codex Usage
5
+ description: Fetches ChatGPT rate limit usage and quota details from the private wham/usage API.
6
+ version: 1.1.3
7
+ servers:
8
+ - url: https://chatgpt.com
9
+ paths: {}
10
+ components:
11
+ securitySchemes:
12
+ chromeSession:
13
+ type: apiKey
14
+ in: cookie
15
+ name: session
16
+ description: "Authenticated via the user's real Chrome session: website-api injects decrypted Chrome
17
+ cookies for chatgpt.com into every request."
18
+ x-website-api:
19
+ id: codex-usage
20
+ domain: chatgpt.com
21
+ cookieDomain: chatgpt.com
22
+ transport: http
23
+ cookies: required
24
+ requiresLogin: true
25
+ imperative: true
26
+ cli:
27
+ command: website-api codex-usage
28
+ positionals: []
29
+ parameters: []
@@ -0,0 +1,20 @@
1
+ import type { SiteContext } from "../../types.js";
2
+ /** Subset of the `/api/organizations` entry fields this site relies on. */
3
+ export interface ClaudeOrg {
4
+ uuid: string;
5
+ name: string;
6
+ /** Set (e.g. "stripe_subscription") on paid orgs, null/absent on free ones. */
7
+ billing_type?: string | null;
8
+ rate_limit_tier?: string;
9
+ capabilities?: string[];
10
+ [key: string]: unknown;
11
+ }
12
+ /** Fetches the organizations visible to the logged-in browser session. */
13
+ export declare function fetchOrganizations(ctx: SiteContext): Promise<ClaudeOrg[]>;
14
+ /** Heuristic: a paid org carries a `billing_type` (e.g. "stripe_subscription"). */
15
+ export declare function isPaidOrg(org: ClaudeOrg): boolean;
16
+ /**
17
+ * Picks the org to query. Honors an explicit selector (matched by uuid or
18
+ * name), otherwise prefers the paid account, falling back to the first org.
19
+ */
20
+ export declare function selectOrg(orgs: ClaudeOrg[], selector?: string): ClaudeOrg;
@@ -0,0 +1,26 @@
1
+ /** Fetches the organizations visible to the logged-in browser session. */
2
+ export async function fetchOrganizations(ctx) {
3
+ const orgs = await ctx.http.json("https://claude.ai/api/organizations");
4
+ if (!Array.isArray(orgs) || orgs.length === 0) {
5
+ throw new Error("No Claude organizations found in browser session");
6
+ }
7
+ return orgs;
8
+ }
9
+ /** Heuristic: a paid org carries a `billing_type` (e.g. "stripe_subscription"). */
10
+ export function isPaidOrg(org) {
11
+ return typeof org.billing_type === "string" && org.billing_type.length > 0;
12
+ }
13
+ /**
14
+ * Picks the org to query. Honors an explicit selector (matched by uuid or
15
+ * name), otherwise prefers the paid account, falling back to the first org.
16
+ */
17
+ export function selectOrg(orgs, selector) {
18
+ if (selector) {
19
+ const s = selector.toLowerCase().trim();
20
+ const match = orgs.find((o) => o.uuid.toLowerCase() === s || o.name?.toLowerCase() === s);
21
+ if (!match)
22
+ throw new Error(`No Claude organization matching "${selector}"`);
23
+ return match;
24
+ }
25
+ return orgs.find(isPaidOrg) ?? orgs[0];
26
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../types.js").Site[];
2
+ export default _default;