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,42 @@
1
+ import { defineSite } from "../../core/define-site.js";
2
+ import { fetchOrganizations, selectOrg } from "./claude-helpers.js";
3
+ /**
4
+ * claude.ai exposes two related endpoints:
5
+ * 1. `GET /api/organizations` → the orgs visible to the session
6
+ * 2. `GET /api/organizations/{id}/usage` → usage percentages + reset times
7
+ *
8
+ * `claude-orgs` exposes the first directly; `claude-usage` chains it — it
9
+ * resolves the org id (the paid account by default) and then fetches usage.
10
+ */
11
+ /** First endpoint: list the organizations available to the browser session. */
12
+ const organizations = defineSite({
13
+ id: "claude-orgs",
14
+ name: "Claude Organizations",
15
+ domain: "claude.ai",
16
+ description: "Lists the Claude organizations available to the logged-in browser session.",
17
+ endpoints: [{ url: "https://claude.ai/api/organizations" }],
18
+ });
19
+ /** Second endpoint: usage for an org, defaulting to the paid account. */
20
+ const usage = defineSite({
21
+ id: "claude-usage",
22
+ name: "Claude Usage",
23
+ domain: "claude.ai",
24
+ description: "Fetches Claude usage percentages and reset times for an organization (the paid account by default).",
25
+ parameters: [
26
+ {
27
+ name: "org",
28
+ type: "string",
29
+ description: "Organization UUID or name to query (defaults to the paid account, else the first org).",
30
+ },
31
+ ],
32
+ run: async (ctx) => {
33
+ const orgs = await fetchOrganizations(ctx);
34
+ const org = selectOrg(orgs, ctx.options.org);
35
+ const usage = await ctx.http.json(`https://claude.ai/api/organizations/${org.uuid}/usage`);
36
+ return {
37
+ organization: { uuid: org.uuid, name: org.name, billing_type: org.billing_type ?? null },
38
+ usage,
39
+ };
40
+ },
41
+ });
42
+ export default [organizations, usage];
@@ -0,0 +1,54 @@
1
+ # Generated by `pnpm generate:openapi` — do not edit by hand.
2
+ openapi: 3.1.0
3
+ info:
4
+ title: Claude Organizations
5
+ description: Lists the Claude organizations available to the logged-in browser session.
6
+ version: 1.1.3
7
+ servers:
8
+ - url: https://claude.ai
9
+ paths:
10
+ /api/organizations:
11
+ get:
12
+ summary: "Claude Organizations: GET /api/organizations"
13
+ description: Lists the Claude organizations available to the logged-in browser session.
14
+ operationId: claude_orgs_get__api_organizations
15
+ responses:
16
+ "200":
17
+ description: JSON response body (shape defined by the site, see its transform)
18
+ security:
19
+ - chromeSession: []
20
+ components:
21
+ securitySchemes:
22
+ chromeSession:
23
+ type: apiKey
24
+ in: cookie
25
+ name: session
26
+ description: "Authenticated via the user's real Chrome session: website-api injects decrypted Chrome
27
+ cookies for claude.ai into every request."
28
+ x-website-api:
29
+ - id: claude-orgs
30
+ domain: claude.ai
31
+ cookieDomain: claude.ai
32
+ transport: http
33
+ cookies: required
34
+ requiresLogin: true
35
+ imperative: false
36
+ cli:
37
+ command: website-api claude-orgs
38
+ positionals: []
39
+ parameters: []
40
+ - id: claude-usage
41
+ domain: claude.ai
42
+ cookieDomain: claude.ai
43
+ transport: http
44
+ cookies: required
45
+ requiresLogin: true
46
+ imperative: true
47
+ cli:
48
+ command: website-api claude-usage
49
+ positionals: []
50
+ parameters:
51
+ - flag: --org
52
+ type: string
53
+ description: Organization UUID or name to query (defaults to the paid account, else the first org).
54
+ required: false
@@ -1 +1,12 @@
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"}]});
1
+ import { defineSite } from "../../core/define-site.js";
2
+ /**
3
+ * Cursor.com — active usage summary. Pure declarative single-endpoint site;
4
+ * the runtime injects cookies + User-Agent and parses JSON.
5
+ */
6
+ export default defineSite({
7
+ id: "cursor-usage",
8
+ name: "Cursor Usage",
9
+ domain: "cursor.com",
10
+ description: "Fetches the active Cursor usage summary from the private usage-summary API.",
11
+ endpoints: [{ url: "https://cursor.com/api/usage-summary" }],
12
+ });
@@ -0,0 +1,39 @@
1
+ # Generated by `pnpm generate:openapi` — do not edit by hand.
2
+ openapi: 3.1.0
3
+ info:
4
+ title: Cursor Usage
5
+ description: Fetches the active Cursor usage summary from the private usage-summary API.
6
+ version: 1.1.3
7
+ servers:
8
+ - url: https://cursor.com
9
+ paths:
10
+ /api/usage-summary:
11
+ get:
12
+ summary: "Cursor Usage: GET /api/usage-summary"
13
+ description: Fetches the active Cursor usage summary from the private usage-summary API.
14
+ operationId: cursor_usage_get__api_usage_summary
15
+ responses:
16
+ "200":
17
+ description: JSON response body (shape defined by the site, see its transform)
18
+ security:
19
+ - chromeSession: []
20
+ components:
21
+ securitySchemes:
22
+ chromeSession:
23
+ type: apiKey
24
+ in: cookie
25
+ name: session
26
+ description: "Authenticated via the user's real Chrome session: website-api injects decrypted Chrome
27
+ cookies for cursor.com into every request."
28
+ x-website-api:
29
+ id: cursor-usage
30
+ domain: cursor.com
31
+ cookieDomain: cursor.com
32
+ transport: http
33
+ cookies: required
34
+ requiresLogin: true
35
+ imperative: false
36
+ cli:
37
+ command: website-api cursor-usage
38
+ positionals: []
39
+ parameters: []
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../types.js").Site;
2
+ export default _default;
@@ -0,0 +1,344 @@
1
+ import { defineSite } from "../../core/define-site.js";
2
+ const ORIGIN = "https://www.e-zpassny.com";
3
+ const TRANSACTIONS_URL = `${ORIGIN}/ezpass/dashboard/transactions`;
4
+ const SIGN_IN_URL = `${ORIGIN}/ezpass/sign-in`;
5
+ const TRANSACTION_API = "https://api.e-zpassny.com/api/account/transaction";
6
+ const STATUS_API = "https://api.e-zpassny.com/api/account/statusCall";
7
+ const STATEMENT_API = "https://api.e-zpassny.com/api/account/statement";
8
+ const VIEW_STATEMENT_API = "https://api.e-zpassny.com/api/accounts/viewStatement";
9
+ /**
10
+ * E-ZPass NY is a Next.js SPA behind Imperva (Incapsula). Auth is NextAuth: a
11
+ * short-lived (~5h) encrypted session cookie that the front end exchanges for a
12
+ * Bearer `accessToken` via `/api/auth/session`. The session expires quickly, so
13
+ * we never reuse cookies — every run logs in fresh through the real browser
14
+ * (which keeps Imperva happy) and pulls a fresh token.
15
+ *
16
+ * Both toll history and payment history come from the SAME endpoint,
17
+ * `POST /api/account/transaction`. Rows are tagged `isTollorPayment`
18
+ * ("TOLL" | "PAYMENT"). The endpoint rejects any window longer than 3 calendar
19
+ * months, so a longer history is stitched together from consecutive windows
20
+ * (the account allows up to ~730 days / 24 months of lookback).
21
+ */
22
+ const MS_PER_DAY = 86_400_000;
23
+ const MAX_MONTHS = 24;
24
+ function fmt(d) {
25
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
26
+ const dd = String(d.getDate()).padStart(2, "0");
27
+ return `${mm}/${dd}/${d.getFullYear()}`;
28
+ }
29
+ function addDays(d, days) {
30
+ return new Date(d.getTime() + days * MS_PER_DAY);
31
+ }
32
+ /** Subtract whole calendar months, clamping the day to the target month length. */
33
+ function subMonths(d, months) {
34
+ const r = new Date(d.getTime());
35
+ const targetMonth = r.getMonth() - months;
36
+ r.setDate(1);
37
+ r.setMonth(targetMonth);
38
+ const lastDay = new Date(r.getFullYear(), r.getMonth() + 1, 0).getDate();
39
+ r.setDate(Math.min(d.getDate(), lastDay));
40
+ return r;
41
+ }
42
+ /**
43
+ * Splits `[today - months, today]` into windows the API will accept: each at
44
+ * most 3 calendar months wide (its hard limit), newest first.
45
+ */
46
+ function buildWindows(months) {
47
+ const today = new Date();
48
+ const bound = subMonths(today, months);
49
+ const windows = [];
50
+ let end = today;
51
+ // Safety stop well above the 24-month cap (8 windows of 3 months = 24 months).
52
+ for (let i = 0; i < 16 && end.getTime() > bound.getTime(); i++) {
53
+ // Maximal safe span: 3 calendar months back, then +1 day so we stay strictly
54
+ // under the "more than 3 months prior" limit.
55
+ let start = addDays(subMonths(end, 3), 1);
56
+ if (start.getTime() < bound.getTime())
57
+ start = bound;
58
+ windows.push({ start: fmt(start), end: fmt(end) });
59
+ end = addDays(start, -1);
60
+ }
61
+ return windows;
62
+ }
63
+ /** Reads the NextAuth Bearer token from the authenticated page session. */
64
+ async function getAccessToken(page) {
65
+ const token = await page.evaluate(async () => {
66
+ const res = await fetch("/api/auth/session", { headers: { accept: "application/json" } });
67
+ const json = (await res.json());
68
+ return json?.accessToken ?? "";
69
+ });
70
+ if (!token) {
71
+ throw new Error("No E-ZPass access token — the session is not authenticated. Ensure a saved Chrome password for e-zpassny.com, or complete login in the attached browser.");
72
+ }
73
+ return token;
74
+ }
75
+ /** Fetches every transaction (tolls + payments) across the requested windows. */
76
+ async function fetchTransactions(page, token, windows, filter) {
77
+ return page.evaluate(async ({ apiUrl, token, windows, filter }) => {
78
+ const all = [];
79
+ let address = null;
80
+ for (const w of windows) {
81
+ const body = {
82
+ agency: "",
83
+ startIndex: "1",
84
+ count: 1000,
85
+ startDate: w.start,
86
+ endDate: w.end,
87
+ searchDate: "Transaction date",
88
+ type: "",
89
+ transponderOrPlateNumber: filter.tag ?? "",
90
+ transactionType: "ALL",
91
+ };
92
+ const res = await fetch(apiUrl, {
93
+ method: "POST",
94
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
95
+ body: JSON.stringify(body),
96
+ });
97
+ const json = (await res.json());
98
+ if (!res.ok || json.message !== "SUCCESS") {
99
+ throw new Error(`Transaction API failed for ${w.start}–${w.end}: ${res.status} ${json.message ?? ""}`.trim());
100
+ }
101
+ const list = json.transactionList;
102
+ if (list?.address && !address)
103
+ address = list.address;
104
+ for (const t of list?.transaction ?? [])
105
+ all.push(t);
106
+ }
107
+ return { rows: all, address };
108
+ }, { apiUrl: TRANSACTION_API, token, windows, filter });
109
+ }
110
+ /** Fetches the account summary (balance, plan, contact) — best effort. */
111
+ async function fetchStatus(page, token) {
112
+ return page.evaluate(async ({ apiUrl, token }) => {
113
+ try {
114
+ const res = await fetch(apiUrl, { headers: { authorization: `Bearer ${token}` } });
115
+ if (!res.ok)
116
+ return null;
117
+ return (await res.json());
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }, { apiUrl: STATUS_API, token });
123
+ }
124
+ /**
125
+ * Lists account statements (monthly PDFs) in a date range. Unlike the
126
+ * transaction endpoint this accepts the full lookback in a single call.
127
+ */
128
+ async function fetchStatements(page, token, range) {
129
+ return page.evaluate(async ({ apiUrl, token, range }) => {
130
+ const res = await fetch(apiUrl, {
131
+ method: "POST",
132
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
133
+ body: JSON.stringify({
134
+ count: "200",
135
+ startIndex: "1",
136
+ startDate: range.start,
137
+ endDate: range.end,
138
+ }),
139
+ });
140
+ const json = (await res.json());
141
+ if (!res.ok || json.message !== "SUCCESS") {
142
+ throw new Error(`Statement API failed: ${res.status} ${json.message ?? ""}`.trim());
143
+ }
144
+ return json.statementResponse?.statementList ?? [];
145
+ }, { apiUrl: STATEMENT_API, token, range });
146
+ }
147
+ /** Downloads one statement PDF and returns its bytes as a base64 string. */
148
+ async function downloadStatementB64(page, token, fileName) {
149
+ return page.evaluate(async ({ apiUrl, token, fileName }) => {
150
+ const url = `${apiUrl}?fileId=${encodeURIComponent(fileName)}`;
151
+ const res = await fetch(url, { headers: { authorization: `Bearer ${token}` } });
152
+ if (!res.ok)
153
+ throw new Error(`Statement download failed for ${fileName}: ${res.status}`);
154
+ const bytes = new Uint8Array(await res.arrayBuffer());
155
+ let binary = "";
156
+ const chunk = 0x8000;
157
+ for (let i = 0; i < bytes.length; i += chunk) {
158
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
159
+ }
160
+ return btoa(binary);
161
+ }, { apiUrl: VIEW_STATEMENT_API, token, fileName });
162
+ }
163
+ /** Resolves a `--download` target into the statements to fetch. */
164
+ function resolveDownloadTargets(statements, target) {
165
+ const clean = target.trim();
166
+ if (clean.toLowerCase() === "all")
167
+ return statements;
168
+ // A bare number is an index (matching the UI's 1-based displayIndex), NOT a
169
+ // filename substring — every filename contains digits.
170
+ if (/^\d+$/.test(clean)) {
171
+ const byDisplay = statements.find((s) => s.displayIndex === clean);
172
+ if (byDisplay)
173
+ return [byDisplay];
174
+ const idx = Number(clean);
175
+ if (idx >= 1 && idx <= statements.length)
176
+ return [statements[idx - 1]];
177
+ return [];
178
+ }
179
+ // Otherwise treat it as an exact / partial filename match.
180
+ return statements.filter((s) => s.fileName.toLowerCase().includes(clean.toLowerCase()));
181
+ }
182
+ /** De-dupes by stable id and sorts newest first. */
183
+ function dedupeAndSort(rows) {
184
+ const seen = new Map();
185
+ for (const r of rows) {
186
+ const key = String(r.compositeId || r.transactionNumber || JSON.stringify(r));
187
+ if (!seen.has(key))
188
+ seen.set(key, r);
189
+ }
190
+ const parse = (s) => {
191
+ // "MM/DD/YYYY HH:mm:ss.SSS" → comparable timestamp.
192
+ const [date, time = "00:00:00.000"] = String(s).split(" ");
193
+ const [mm, dd, yyyy] = date.split("/").map(Number);
194
+ const t = Date.parse(`${yyyy}-${mm}-${dd}T${time}`);
195
+ return Number.isNaN(t) ? 0 : t;
196
+ };
197
+ return [...seen.values()].sort((a, b) => parse(b.transactionDate) - parse(a.transactionDate));
198
+ }
199
+ export default defineSite({
200
+ id: "ezpassny",
201
+ name: "E-ZPass New York",
202
+ domain: "e-zpassny.com",
203
+ description: "Fetches E-ZPass NY toll/payment history, lists account statements, and downloads statement PDFs (browser transport, logs in fresh each run via your saved Chrome password).",
204
+ transport: "browser",
205
+ cookies: "optional",
206
+ // Keep the authenticated tab open so the short-lived session stays warm.
207
+ keepBrowserOpen: true,
208
+ // Declarative single-page login form (captured from /ezpass/sign-in).
209
+ auth: {
210
+ intendedUrl: TRANSACTIONS_URL,
211
+ loginUrl: SIGN_IN_URL,
212
+ emailSelector: "#login",
213
+ passwordSelector: "#password",
214
+ submitButtonSelector: 'button[type="submit"]',
215
+ expectedRedirectUrlPattern: "**/ezpass/dashboard**",
216
+ delayMs: 1000,
217
+ },
218
+ parameters: [
219
+ {
220
+ name: "months",
221
+ type: "number",
222
+ description: `Months of history to fetch, 1-${MAX_MONTHS} (default 3)`,
223
+ default: 3,
224
+ short: "m",
225
+ },
226
+ {
227
+ name: "payments",
228
+ type: "boolean",
229
+ description: "Only payment-history rows (exclude tolls)",
230
+ short: "p",
231
+ },
232
+ {
233
+ name: "tolls",
234
+ type: "boolean",
235
+ description: "Only toll-history rows (exclude payments)",
236
+ short: "t",
237
+ },
238
+ {
239
+ name: "tag",
240
+ type: "string",
241
+ description: "Filter by transponder or license-plate number",
242
+ },
243
+ {
244
+ name: "limit",
245
+ type: "number",
246
+ description: "Cap the number of rows returned",
247
+ short: "l",
248
+ },
249
+ {
250
+ name: "summary",
251
+ type: "boolean",
252
+ description: "Include the account summary (balance, plan, contact)",
253
+ short: "s",
254
+ },
255
+ {
256
+ name: "statements",
257
+ type: "boolean",
258
+ description: "List account statements (monthly PDFs) instead of transactions",
259
+ short: "S",
260
+ },
261
+ {
262
+ name: "download",
263
+ type: "string",
264
+ description: 'Download statement PDF(s): a filename, a 1-based index, or "all". Saved to --out-dir or cwd',
265
+ short: "d",
266
+ },
267
+ {
268
+ name: "out-dir",
269
+ type: "string",
270
+ description: "Directory to save downloaded statement PDFs (default: current directory)",
271
+ },
272
+ ],
273
+ run: async (ctx) => {
274
+ // Triggers CDP connect + fingerprint + auto-login (via the auth config).
275
+ const page = await ctx.browser();
276
+ // Make sure we're on an e-zpassny.com origin so the API calls share cookies.
277
+ if (!page.url().startsWith(ORIGIN)) {
278
+ await page.goto(TRANSACTIONS_URL, { waitUntil: "domcontentloaded" });
279
+ }
280
+ const months = Math.min(Math.max(Number(ctx.options.months) || 3, 1), MAX_MONTHS);
281
+ const tag = ctx.options.tag ? String(ctx.options.tag).trim() : undefined;
282
+ const token = await getAccessToken(page);
283
+ // ── Statements: list, or download PDF(s) ──
284
+ const wantsStatements = ctx.options.statements || ctx.options.download !== undefined;
285
+ if (wantsStatements) {
286
+ const today = new Date();
287
+ const range = { start: fmt(subMonths(today, months)), end: fmt(today) };
288
+ const statements = await fetchStatements(page, token, range);
289
+ if (ctx.options.download === undefined) {
290
+ return {
291
+ range: { months, ...range },
292
+ count: statements.length,
293
+ statements,
294
+ };
295
+ }
296
+ const targets = resolveDownloadTargets(statements, String(ctx.options.download));
297
+ if (!targets.length) {
298
+ const avail = statements.map((s, i) => `${i + 1}) ${s.fileName} (${s.statementPeriod})`).join("\n");
299
+ throw new Error(`No statement matched "${ctx.options.download}". Available in the last ${months} month(s):\n${avail || "(none)"}`);
300
+ }
301
+ const downloaded = [];
302
+ for (const s of targets) {
303
+ if (ctx.debug)
304
+ console.log(`Downloading ${s.fileName} (${s.statementPeriod})...`);
305
+ const b64 = await downloadStatementB64(page, token, s.fileName);
306
+ const buf = Buffer.from(b64, "base64");
307
+ const path = await ctx.save(s.fileName, buf);
308
+ downloaded.push({
309
+ fileName: s.fileName,
310
+ statementPeriod: s.statementPeriod,
311
+ bytes: buf.length,
312
+ path,
313
+ });
314
+ }
315
+ return { downloaded: downloaded.length, files: downloaded };
316
+ }
317
+ // ── Transactions (tolls + payments) ──
318
+ const windows = buildWindows(months);
319
+ if (ctx.debug)
320
+ console.log(`Fetching ${months} month(s) across ${windows.length} window(s)...`);
321
+ const { rows, address } = await fetchTransactions(page, token, windows, { tag });
322
+ let transactions = dedupeAndSort(rows);
323
+ if (ctx.options.payments && !ctx.options.tolls) {
324
+ transactions = transactions.filter((t) => t.isTollorPayment === "PAYMENT");
325
+ }
326
+ else if (ctx.options.tolls && !ctx.options.payments) {
327
+ transactions = transactions.filter((t) => t.isTollorPayment === "TOLL");
328
+ }
329
+ if (ctx.options.limit)
330
+ transactions = transactions.slice(0, Number(ctx.options.limit));
331
+ const tollCount = transactions.filter((t) => t.isTollorPayment === "TOLL").length;
332
+ const paymentCount = transactions.filter((t) => t.isTollorPayment === "PAYMENT").length;
333
+ const result = {
334
+ account: address,
335
+ range: { months, from: windows.at(-1)?.start, to: windows[0]?.end },
336
+ counts: { total: transactions.length, tolls: tollCount, payments: paymentCount },
337
+ transactions,
338
+ };
339
+ if (ctx.options.summary) {
340
+ result.summary = await fetchStatus(page, token);
341
+ }
342
+ return result;
343
+ },
344
+ });
@@ -0,0 +1,68 @@
1
+ # Generated by `pnpm generate:openapi` — do not edit by hand.
2
+ openapi: 3.1.0
3
+ info:
4
+ title: E-ZPass New York
5
+ description: Fetches E-ZPass NY toll/payment history, lists account statements, and downloads
6
+ statement PDFs (browser transport, logs in fresh each run via your saved Chrome password).
7
+ version: 1.1.3
8
+ servers:
9
+ - url: https://e-zpassny.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 e-zpassny.com into every request."
19
+ x-website-api:
20
+ id: ezpassny
21
+ domain: e-zpassny.com
22
+ cookieDomain: e-zpassny.com
23
+ transport: browser
24
+ cookies: optional
25
+ requiresLogin: true
26
+ imperative: true
27
+ cli:
28
+ command: website-api ezpassny
29
+ positionals: []
30
+ parameters:
31
+ - flag: --months
32
+ type: number
33
+ description: Months of history to fetch, 1-24 (default 3)
34
+ default: 3
35
+ required: false
36
+ - flag: --payments
37
+ type: boolean
38
+ description: Only payment-history rows (exclude tolls)
39
+ required: false
40
+ - flag: --tolls
41
+ type: boolean
42
+ description: Only toll-history rows (exclude payments)
43
+ required: false
44
+ - flag: --tag
45
+ type: string
46
+ description: Filter by transponder or license-plate number
47
+ required: false
48
+ - flag: --limit
49
+ type: number
50
+ description: Cap the number of rows returned
51
+ required: false
52
+ - flag: --summary
53
+ type: boolean
54
+ description: Include the account summary (balance, plan, contact)
55
+ required: false
56
+ - flag: --statements
57
+ type: boolean
58
+ description: List account statements (monthly PDFs) instead of transactions
59
+ required: false
60
+ - flag: --download
61
+ type: string
62
+ description: 'Download statement PDF(s): a filename, a 1-based index, or "all". Saved to --out-dir
63
+ or cwd'
64
+ required: false
65
+ - flag: --out-dir
66
+ type: string
67
+ description: "Directory to save downloaded statement PDFs (default: current directory)"
68
+ required: false
@@ -1,5 +1,16 @@
1
1
  import { type UniversalGoogleSchema } from "../../util/google-json.js";
2
2
  /** Schema mapping Gemini's batchexecute response indices to a usage summary. */
3
3
  export declare const GEMINI_USAGE_SCHEMA: UniversalGoogleSchema;
4
+ /**
5
+ * Pulls the per-session params Gemini embeds in its app HTML (`WIZ_global_data`):
6
+ * SNlM0e → `at` anti-forgery token (required for the POST)
7
+ * cfb2h → `bl` backend release id
8
+ * FdrFJe → `f.sid` session id
9
+ */
10
+ export declare function extractWizParams(html: string): {
11
+ at: string | null;
12
+ bl: string | null;
13
+ fsid: string | null;
14
+ };
4
15
  declare const _default: import("../../types.js").Site;
5
16
  export default _default;
@@ -1 +1,80 @@
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)}});
1
+ import { defineSite } from "../../core/define-site.js";
2
+ import { decodeGoogleJsonWithSchema } from "../../util/google-json.js";
3
+ /** Schema mapping Gemini's batchexecute response indices to a usage summary. */
4
+ export const GEMINI_USAGE_SCHEMA = {
5
+ planCode: { path: [0] },
6
+ planName: {
7
+ path: [0],
8
+ transform: (val) => (val === 2 ? "Gemini Pro" : val === 1 ? "Gemini Free" : `Unknown (${val})`),
9
+ },
10
+ limits: {
11
+ path: [1],
12
+ items: {
13
+ rawLimit: { path: [0] },
14
+ percentageUsed: { path: [1], transform: (val) => parseFloat((val * 100).toFixed(2)) },
15
+ tierName: {
16
+ path: [2],
17
+ transform: (val) => val === 1 ? "Hourly Usage Limit" : val === 2 ? "Weekly Usage Limit" : `Unknown Tier (${val})`,
18
+ },
19
+ resetTime: {
20
+ path: [3, 0, 0],
21
+ transform: (val) => (val ? new Date(val * 1000).toISOString() : null),
22
+ },
23
+ },
24
+ },
25
+ };
26
+ /** The batchexecute RPC the `/usage` page issues for quota data. */
27
+ const RPC_ID = "jSf9Qc";
28
+ /**
29
+ * Pulls the per-session params Gemini embeds in its app HTML (`WIZ_global_data`):
30
+ * SNlM0e → `at` anti-forgery token (required for the POST)
31
+ * cfb2h → `bl` backend release id
32
+ * FdrFJe → `f.sid` session id
33
+ */
34
+ export function extractWizParams(html) {
35
+ const grab = (key) => {
36
+ const match = html.match(new RegExp(`"${key}":"(.*?)"`));
37
+ return match ? match[1] : null;
38
+ };
39
+ return { at: grab("SNlM0e"), bl: grab("cfb2h"), fsid: grab("FdrFJe") };
40
+ }
41
+ export default defineSite({
42
+ id: "gemini-usage",
43
+ name: "Gemini Usage",
44
+ domain: "gemini.google.com",
45
+ // Auth cookies are scoped to google.com, not the gemini subdomain.
46
+ cookieDomain: "google.com",
47
+ description: "Fetches Gemini account usage/quota details directly over HTTP (no browser).",
48
+ cookies: "required",
49
+ endpoints: [{ url: "https://gemini.google.com/usage" }],
50
+ run: async (ctx) => {
51
+ // 1. Fetch the app shell to read this session's anti-forgery token + backend params.
52
+ const html = await ctx.http.text("https://gemini.google.com/app");
53
+ const { at, bl, fsid } = extractWizParams(html);
54
+ if (!at) {
55
+ throw new Error("Could not find Gemini session token (SNlM0e). Make sure you are logged into gemini.google.com in Chrome.");
56
+ }
57
+ // 2. Replay the batchexecute RPC the /usage page makes for quota data.
58
+ const query = new URLSearchParams({ rpcids: RPC_ID, "source-path": "/usage", hl: "en", rt: "c" });
59
+ if (bl)
60
+ query.set("bl", bl);
61
+ if (fsid)
62
+ query.set("f.sid", fsid);
63
+ const body = new URLSearchParams({
64
+ "f.req": JSON.stringify([[[RPC_ID, "[]", null, "generic"]]]),
65
+ at,
66
+ });
67
+ if (ctx.debug)
68
+ console.log("Posting Gemini usage batchexecute request...");
69
+ const raw = await ctx.http.text(`https://gemini.google.com/_/BardChatUi/data/batchexecute?${query.toString()}`, {
70
+ method: "POST",
71
+ headers: {
72
+ "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
73
+ "x-same-domain": "1",
74
+ referer: "https://gemini.google.com/",
75
+ },
76
+ body: body.toString(),
77
+ });
78
+ return decodeGoogleJsonWithSchema(raw, RPC_ID, GEMINI_USAGE_SCHEMA);
79
+ },
80
+ });