website-api 1.1.3 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -1
- package/dist/bin/cli.js +209 -1
- package/dist/src/capabilities/browser.d.ts +16 -2
- package/dist/src/capabilities/browser.js +158 -1
- package/dist/src/capabilities/cookies.d.ts +7 -1
- package/dist/src/capabilities/cookies.js +68 -1
- package/dist/src/capabilities/download.js +32 -1
- package/dist/src/capabilities/fingerprint.js +62 -1
- package/dist/src/capabilities/http.js +101 -1
- package/dist/src/capabilities/login/login-helper.js +185 -1
- package/dist/src/capabilities/login/login-strategy.js +36 -1
- package/dist/src/challenges/perimeterx.d.ts +62 -0
- package/dist/src/challenges/perimeterx.js +112 -0
- package/dist/src/cli/ext.js +338 -1
- package/dist/src/core/context.d.ts +2 -2
- package/dist/src/core/context.js +138 -1
- package/dist/src/core/define-site.js +74 -1
- package/dist/src/core/loader.js +142 -1
- package/dist/src/core/registry.js +332 -1
- package/dist/src/core/runtime.d.ts +12 -4
- package/dist/src/core/runtime.js +98 -1
- package/dist/src/env.js +34 -1
- package/dist/src/sites/bloomberg.com/index.d.ts +11 -0
- package/dist/src/sites/bloomberg.com/index.js +49 -0
- package/dist/src/sites/bloomberg.com/openapi.yaml +38 -0
- package/dist/src/sites/chase.com/download-helper.js +266 -1
- package/dist/src/sites/chase.com/index.js +87 -1
- package/dist/src/sites/chase.com/openapi.yaml +76 -0
- package/dist/src/sites/chatgpt.com/index.js +24 -1
- package/dist/src/sites/chatgpt.com/openapi.yaml +29 -0
- package/dist/src/sites/claude.ai/claude-helpers.js +26 -1
- package/dist/src/sites/claude.ai/index.js +42 -1
- package/dist/src/sites/claude.ai/openapi.yaml +54 -0
- package/dist/src/sites/cursor.com/index.js +12 -1
- package/dist/src/sites/cursor.com/openapi.yaml +39 -0
- package/dist/src/sites/e-zpassny.com/index.d.ts +2 -0
- package/dist/src/sites/e-zpassny.com/index.js +344 -0
- package/dist/src/sites/e-zpassny.com/openapi.yaml +68 -0
- package/dist/src/sites/gemini.google.com/index.js +80 -1
- package/dist/src/sites/gemini.google.com/openapi.yaml +39 -0
- package/dist/src/sites/google.com/google-helpers.js +255 -1
- package/dist/src/sites/google.com/index.js +253 -1
- package/dist/src/sites/google.com/openapi.yaml +59 -0
- package/dist/src/sites/microcenter.com/openapi.yaml +44 -0
- package/dist/src/sites/ollama.com/index.js +43 -1
- package/dist/src/sites/ollama.com/openapi.yaml +39 -0
- package/dist/src/sites/perplexity.ai/index.js +253 -1
- package/dist/src/sites/perplexity.ai/openapi.yaml +51 -0
- package/dist/src/sites/pseg.com/index.js +243 -1
- package/dist/src/sites/pseg.com/openapi.yaml +42 -0
- package/dist/src/sites/pseg.com/pseg-helpers.js +53 -1
- package/dist/src/sites/voice.google.com/index.d.ts +2 -0
- package/dist/src/sites/voice.google.com/index.js +122 -0
- package/dist/src/sites/voice.google.com/openapi.yaml +67 -0
- package/dist/src/sites/voice.google.com/voice-helpers.d.ts +105 -0
- package/dist/src/sites/voice.google.com/voice-helpers.js +181 -0
- package/dist/src/sites/zillow.com/index.d.ts +2 -0
- package/dist/src/sites/zillow.com/index.js +303 -0
- package/dist/src/sites/zillow.com/openapi.yaml +55 -0
- package/dist/src/types.d.ts +7 -0
- package/dist/src/types.js +1 -1
- package/dist/src/util/args-parser.js +150 -1
- package/dist/src/util/google-json.js +74 -1
- package/dist/src/website-api.d.ts +7 -7
- package/dist/src/website-api.js +13 -1
- package/package.json +38 -10
|
@@ -1 +1,42 @@
|
|
|
1
|
-
import{defineSite
|
|
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.4
|
|
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
|
|
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.4
|
|
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,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.4
|
|
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 +1,80 @@
|
|
|
1
|
-
import{defineSite
|
|
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
|
+
});
|