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
@@ -1 +1,53 @@
1
- const e={15:"15-Minute","15-minute":"15-Minute",30:"30-Minute","30-minute":"30-Minute",hourly:"Hourly",daily:"Daily",weekly:"Weekly",monthly:"Monthly",billing:"Billing"},r={"15-Minute":"3","30-Minute":"4",Hourly:"5",Daily:"6",Weekly:"8",Monthly:"9",Billing:"7"};export function normalizeInterval(r){const t=String(r||"").trim().toLowerCase(),n=e[t];if(!n)throw new Error(`Invalid interval: ${r}. Use 15, 30, hourly, daily, weekly, monthly, or billing.`);return n}export function intervalToValue(e){return r[e]}export function inferServiceTypeFromPropertyTitle(e){if(/\bgas\b/i.test(e))return"Gas";if(/\belectric\b/i.test(e))return"Electric";throw new Error(`Could not infer service type from property title: ${e}`)}export function extractAddressNumber(e){const r=String(e||"").match(/^(\d+)/);return r?r[1]:""}export function formatPropertyLabel(e){const r=String(e.title||"").trim();if(r)return r;const t=e.propertyType||inferServiceTypeFromPropertyTitle(e.title);return`${extractAddressNumber(e.address)||e.propertyId||"Unknown"} ${t}`}
1
+ // Pure PSEG helpers no Playwright, directly unit-testable.
2
+ const INTERVAL_MAP = {
3
+ "15": "15-Minute",
4
+ "15-minute": "15-Minute",
5
+ "30": "30-Minute",
6
+ "30-minute": "30-Minute",
7
+ hourly: "Hourly",
8
+ daily: "Daily",
9
+ weekly: "Weekly",
10
+ monthly: "Monthly",
11
+ billing: "Billing",
12
+ };
13
+ const INTERVAL_TO_VALUE = {
14
+ "15-Minute": "3",
15
+ "30-Minute": "4",
16
+ Hourly: "5",
17
+ Daily: "6",
18
+ Weekly: "8",
19
+ Monthly: "9",
20
+ Billing: "7",
21
+ };
22
+ export function normalizeInterval(value) {
23
+ const raw = String(value || "")
24
+ .trim()
25
+ .toLowerCase();
26
+ const interval = INTERVAL_MAP[raw];
27
+ if (!interval) {
28
+ throw new Error(`Invalid interval: ${value}. Use 15, 30, hourly, daily, weekly, monthly, or billing.`);
29
+ }
30
+ return interval;
31
+ }
32
+ export function intervalToValue(interval) {
33
+ return INTERVAL_TO_VALUE[interval];
34
+ }
35
+ export function inferServiceTypeFromPropertyTitle(title) {
36
+ if (/\bgas\b/i.test(title))
37
+ return "Gas";
38
+ if (/\belectric\b/i.test(title))
39
+ return "Electric";
40
+ throw new Error(`Could not infer service type from property title: ${title}`);
41
+ }
42
+ export function extractAddressNumber(address) {
43
+ const match = String(address || "").match(/^(\d+)/);
44
+ return match ? match[1] : "";
45
+ }
46
+ export function formatPropertyLabel(property) {
47
+ const title = String(property.title || "").trim();
48
+ if (title)
49
+ return title;
50
+ const propertyType = property.propertyType || inferServiceTypeFromPropertyTitle(property.title);
51
+ const addressNumber = extractAddressNumber(property.address) || property.propertyId || "Unknown";
52
+ return `${addressNumber} ${propertyType}`;
53
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../types.js").Site;
2
+ export default _default;
@@ -0,0 +1,122 @@
1
+ import { defineSite } from "../../core/define-site.js";
2
+ import { buildListBody, buildSapisidAuth, decodeSendResult, decodeThreadList, normalizeNumber, SEND_SMS_PATH, selectGoogleCookies, threadUrl, toThreadId, voiceHeaders, voiceUrl, } from "./voice-helpers.js";
3
+ /**
4
+ * POSTs a JSON+protobuf body to a voiceclient method and returns the parsed
5
+ * array. Cookies are filtered to google.com + deduped, and the SAPISIDHASH is
6
+ * recomputed per call. We pass our own `Cookie` header so `ctx.http` doesn't
7
+ * inject the raw (cross-domain, duplicated) cookie set.
8
+ */
9
+ async function callVoice(ctx, method, body) {
10
+ const { header, byName } = selectGoogleCookies(ctx.cookies());
11
+ const timestamp = Math.floor(Date.now() / 1000);
12
+ const authorization = buildSapisidAuth(byName, timestamp);
13
+ const raw = await ctx.http.text(voiceUrl(method), {
14
+ method: "POST",
15
+ headers: voiceHeaders(authorization, header),
16
+ body: JSON.stringify(body),
17
+ });
18
+ if (ctx.debug)
19
+ console.error(`[voice] ${method} ->`, raw.slice(0, 200));
20
+ try {
21
+ return JSON.parse(raw);
22
+ }
23
+ catch {
24
+ throw new Error(`Google Voice ${method} returned a non-JSON response (are you logged in?): ${raw.slice(0, 160)}`);
25
+ }
26
+ }
27
+ export default defineSite({
28
+ id: "google-voice",
29
+ name: "Google Voice",
30
+ domain: "voice.google.com",
31
+ // Session cookies (SID, SAPISID, *PAPISID) are scoped to google.com, not the subdomain.
32
+ cookieDomain: "google.com",
33
+ description: "List threads and read conversations over HTTP (no browser); send SMS via the attached browser.",
34
+ cookies: "required",
35
+ endpoints: [{ url: "https://voice.google.com/u/0/messages" }],
36
+ positionals: [
37
+ { name: "action", description: "list | read | send (default: list)", required: false },
38
+ { name: "target", description: "phone number or thread id (for read/send)", required: false },
39
+ { name: "message", description: "message text to send (for send)", required: false, variadic: true },
40
+ ],
41
+ parameters: [
42
+ { name: "limit", type: "number", description: "max threads to return", default: 25, short: "n" },
43
+ {
44
+ name: "messages",
45
+ type: "number",
46
+ description: "max messages per thread to fetch",
47
+ default: 15,
48
+ short: "m",
49
+ },
50
+ {
51
+ name: "authuser",
52
+ type: "number",
53
+ description: "Google account index in the URL (/u/N/) for send",
54
+ default: 0,
55
+ },
56
+ ],
57
+ run: async (ctx) => {
58
+ const action = String(ctx.options.action || "list").toLowerCase();
59
+ const threadLimit = Number(ctx.options.limit) || 25;
60
+ const perThread = Number(ctx.options.messages) || 15;
61
+ if (action === "list") {
62
+ const body = await callVoice(ctx, "api2thread/list", buildListBody(threadLimit, perThread));
63
+ return decodeThreadList(body)
64
+ .slice(0, threadLimit)
65
+ .map(({ messages, ...summary }) => summary);
66
+ }
67
+ if (action === "read") {
68
+ const target = ctx.options.target;
69
+ if (!target)
70
+ throw new Error("read requires a phone number or thread id, e.g. `google-voice read +17185139789`");
71
+ const wantedId = toThreadId(String(target));
72
+ const body = await callVoice(ctx, "api2thread/list", buildListBody(threadLimit, perThread));
73
+ const thread = decodeThreadList(body).find((t) => t.threadId === wantedId);
74
+ if (!thread) {
75
+ throw new Error(`No thread found for ${target} (looked for ${wantedId}). It may be older than the latest ${threadLimit} threads.`);
76
+ }
77
+ // Oldest → newest reads more naturally than the API's newest-first order.
78
+ return { ...thread, messages: [...thread.messages].reverse() };
79
+ }
80
+ if (action === "send") {
81
+ const target = ctx.options.target;
82
+ const message = Array.isArray(ctx.options.message)
83
+ ? ctx.options.message.join(" ")
84
+ : ctx.options.message;
85
+ if (!target || !message) {
86
+ throw new Error('send requires a number and text, e.g. `google-voice send +17185139789 "hello"`');
87
+ }
88
+ const text = String(message);
89
+ const authUser = Number(ctx.options.authuser) || 0;
90
+ // Unlike list/read, sendsms is gated by reCAPTCHA Enterprise + a BotGuard
91
+ // token that only Google's in-browser JS can mint — so we drive the
92
+ // authenticated page (which produces those tokens) instead of POSTing
93
+ // directly. Requires Chrome running with remote debugging (CDP).
94
+ let page;
95
+ try {
96
+ page = await ctx.browser();
97
+ }
98
+ catch (err) {
99
+ throw new Error(`send needs a running Chrome (it can't be done over plain HTTP — sendsms requires reCAPTCHA). ` +
100
+ `Start Chrome with --remote-debugging-port=9222. Cause: ${err instanceof Error ? err.message : String(err)}`);
101
+ }
102
+ await page.goto(threadUrl(String(target), authUser), { waitUntil: "domcontentloaded" });
103
+ const box = page.getByRole("textbox", { name: "Type a message" });
104
+ await box.waitFor({ state: "visible", timeout: 20000 });
105
+ await box.click();
106
+ await box.fill(text);
107
+ const waitForSend = page.waitForResponse((r) => r.url().includes(SEND_SMS_PATH) && r.request().method() === "POST", { timeout: 30000 });
108
+ await page.getByRole("button", { name: "Send message" }).click();
109
+ const resp = await waitForSend;
110
+ const sendBody = JSON.parse(await resp.text());
111
+ if (ctx.debug)
112
+ console.error("[voice] sendsms ->", JSON.stringify(sendBody).slice(0, 160));
113
+ return {
114
+ sent: true,
115
+ to: normalizeNumber(String(target)),
116
+ text,
117
+ ...decodeSendResult(sendBody),
118
+ };
119
+ }
120
+ throw new Error(`Unknown action "${action}". Use one of: list, read, send.`);
121
+ },
122
+ });
@@ -0,0 +1,67 @@
1
+ # Generated by `pnpm generate:openapi` — do not edit by hand.
2
+ openapi: 3.1.0
3
+ info:
4
+ title: Google Voice
5
+ description: List threads and read conversations over HTTP (no browser); send SMS via the attached browser.
6
+ version: 1.1.3
7
+ servers:
8
+ - url: https://voice.google.com
9
+ paths:
10
+ /u/0/messages:
11
+ get:
12
+ summary: "Google Voice: GET /u/0/messages"
13
+ description: List threads and read conversations over HTTP (no browser); send SMS via the attached
14
+ browser.
15
+ operationId: google_voice_get__u_0_messages
16
+ responses:
17
+ "200":
18
+ description: JSON response body (shape defined by the site, see its transform)
19
+ security:
20
+ - chromeSession: []
21
+ components:
22
+ securitySchemes:
23
+ chromeSession:
24
+ type: apiKey
25
+ in: cookie
26
+ name: session
27
+ description: "Authenticated via the user's real Chrome session: website-api injects decrypted Chrome
28
+ cookies for google.com into every request."
29
+ x-website-api:
30
+ id: google-voice
31
+ domain: voice.google.com
32
+ cookieDomain: google.com
33
+ transport: http
34
+ cookies: required
35
+ requiresLogin: true
36
+ imperative: false
37
+ cli:
38
+ command: website-api google-voice
39
+ positionals:
40
+ - name: action
41
+ description: "list | read | send (default: list)"
42
+ required: false
43
+ variadic: false
44
+ - name: target
45
+ description: phone number or thread id (for read/send)
46
+ required: false
47
+ variadic: false
48
+ - name: message
49
+ description: message text to send (for send)
50
+ required: false
51
+ variadic: true
52
+ parameters:
53
+ - flag: --limit
54
+ type: number
55
+ description: max threads to return
56
+ default: 25
57
+ required: false
58
+ - flag: --messages
59
+ type: number
60
+ description: max messages per thread to fetch
61
+ default: 15
62
+ required: false
63
+ - flag: --authuser
64
+ type: number
65
+ description: Google account index in the URL (/u/N/) for send
66
+ default: 0
67
+ required: false
@@ -0,0 +1,105 @@
1
+ import type { CookieEntry } from "chrome-tools";
2
+ /**
3
+ * Google Voice's web app fetches data from a private JSON+protobuf API:
4
+ * POST https://clients6.google.com/voice/v1/voiceclient/<method>?alt=protojson&key=<API_KEY>
5
+ *
6
+ * It can be replayed over plain HTTP (no browser) once you mirror the exact
7
+ * cross-domain handshake the gapi client uses — see the long note in
8
+ * `voiceHeaders()`.
9
+ */
10
+ export declare const VOICE_API_BASE = "https://clients6.google.com/voice/v1/voiceclient";
11
+ /** Public web-client API key embedded in voice.google.com's JS (not a secret). */
12
+ export declare const VOICE_API_KEY = "AIzaSyDTYc1N4xiODyrQYK0Kl6g_y279LjYkrBg";
13
+ /** Origin the SAPISIDHASH is bound to (the app's real origin). */
14
+ export declare const VOICE_ORIGIN = "https://voice.google.com";
15
+ /** Host that actually serves the API (and that XD3 wants the Origin to match). */
16
+ export declare const CLIENTS6_ORIGIN = "https://clients6.google.com";
17
+ /** Full request URL for a voiceclient method. */
18
+ export declare function voiceUrl(method: string): string;
19
+ /** Substring identifying the send-SMS response (used when driving the browser). */
20
+ export declare const SEND_SMS_PATH = "/voiceclient/api2thread/sendsms";
21
+ /**
22
+ * `sendsms` is gated by reCAPTCHA Enterprise (this site key) + a BotGuard token,
23
+ * both minted only by Google's in-browser anti-bot JS — so `send` can't go over
24
+ * pure HTTP and must drive the authenticated page instead.
25
+ */
26
+ export declare const RECAPTCHA_SITE_KEY = "6Lfv4cYpAAAAABTKDTZu0jpcgdQ5Ak4XxeUBqA7B";
27
+ export declare const RECAPTCHA_ACTION = "SEND_SMS";
28
+ /** Builds the in-app conversation URL for a given target. */
29
+ export declare function threadUrl(target: string, authUser?: number): string;
30
+ /**
31
+ * Selects the cookies clients6.google.com actually receives: only those scoped
32
+ * to `google.com`, de-duplicated by name. Chrome may hold same-named cookies
33
+ * for sibling domains (e.g. `.youtube.com`) or stale duplicates; sending those
34
+ * alongside — or hashing the wrong `SAPISID` — yields `401 invalid credentials`.
35
+ */
36
+ export declare function selectGoogleCookies(cookies: CookieEntry[]): {
37
+ header: string;
38
+ byName: Map<string, string>;
39
+ };
40
+ /**
41
+ * Builds the `Authorization` header Google's JS clients send to clients6:
42
+ * `SAPISIDHASH <ts>_<sha1(`<ts> <cookie> <origin>`)> SAPISID1PHASH … SAPISID3PHASH …`
43
+ * Every *APISID cookie present is hashed against the *voice.google.com* origin.
44
+ */
45
+ export declare function buildSapisidAuth(byName: Map<string, string>, timestampSeconds: number, origin?: string): string;
46
+ /**
47
+ * Headers for a clients6 voiceclient call. The cross-domain ("XD3") trick:
48
+ * • `Origin` must equal the Host (clients6) — the app issues the XHR from
49
+ * gapi's proxy iframe served *from* clients6, so anything else trips
50
+ * `400 "Origin doesn't match Host for XD3"`.
51
+ * • The real caller identity rides in `X-Origin` (= voice.google.com), which
52
+ * is what the SAPISIDHASH is bound to — anything else trips `401`.
53
+ * Those two together are why a naive same-origin request can't work.
54
+ */
55
+ export declare function voiceHeaders(authorization: string, cookieHeader: string): Record<string, string>;
56
+ /**
57
+ * Normalizes a US-centric phone number to E.164 (`+1XXXXXXXXXX`). Already-prefixed
58
+ * numbers and short codes (e.g. "39769") are passed through untouched.
59
+ */
60
+ export declare function normalizeNumber(input: string): string;
61
+ /**
62
+ * Resolves a thread id from either a raw thread id (`t.+1718…`, `t.39769`) or a
63
+ * phone number / short code (which the web app maps to `t.<E.164-or-shortcode>`).
64
+ */
65
+ export declare function toThreadId(target: string): string;
66
+ export interface VoiceMessage {
67
+ id: string;
68
+ timestamp: string;
69
+ from: string | null;
70
+ text: string | null;
71
+ direction: "incoming" | "outgoing" | "unknown";
72
+ read: boolean;
73
+ }
74
+ export interface VoiceThread {
75
+ threadId: string;
76
+ contact: string | null;
77
+ unread: boolean;
78
+ latestText: string | null;
79
+ latestTimestamp: string | null;
80
+ messageCount: number;
81
+ messages: VoiceMessage[];
82
+ }
83
+ /** Decodes one message tuple from an `api2thread/list` thread. */
84
+ export declare function decodeMessage(m: any[]): VoiceMessage;
85
+ /** Decodes one thread tuple, newest message first (as the API returns it). */
86
+ export declare function decodeThread(t: any[]): VoiceThread;
87
+ /** Decodes a full `api2thread/list` response body into threads. */
88
+ export declare function decodeThreadList(body: any): VoiceThread[];
89
+ /**
90
+ * Body for `api2thread/list`: `[folder, threadLimit, perThreadLimit, null, null, [null,1,1,1]]`.
91
+ * `folder` 2 is the SMS/text inbox the web app loads by default.
92
+ */
93
+ export declare function buildListBody(threadLimit: number, perThreadLimit: number): unknown[];
94
+ /**
95
+ * Body for `api2thread/sendsms`:
96
+ * `[null,null,null,null, text, threadId, null, null, [transactionId]]`
97
+ * `transactionId` is a client idempotency id; any large positive integer works.
98
+ */
99
+ export declare function buildSendBody(text: string, threadId: string, transactionId: number): unknown[];
100
+ /** Decodes an `api2thread/sendsms` response into the sent-message summary. */
101
+ export declare function decodeSendResult(body: any): {
102
+ threadId: string | null;
103
+ messageId: string | null;
104
+ timestamp: string | null;
105
+ };
@@ -0,0 +1,181 @@
1
+ import { createHash } from "node:crypto";
2
+ /**
3
+ * Google Voice's web app fetches data from a private JSON+protobuf API:
4
+ * POST https://clients6.google.com/voice/v1/voiceclient/<method>?alt=protojson&key=<API_KEY>
5
+ *
6
+ * It can be replayed over plain HTTP (no browser) once you mirror the exact
7
+ * cross-domain handshake the gapi client uses — see the long note in
8
+ * `voiceHeaders()`.
9
+ */
10
+ export const VOICE_API_BASE = "https://clients6.google.com/voice/v1/voiceclient";
11
+ /** Public web-client API key embedded in voice.google.com's JS (not a secret). */
12
+ export const VOICE_API_KEY = "AIzaSyDTYc1N4xiODyrQYK0Kl6g_y279LjYkrBg"; // secret-scan: allow
13
+ /** Origin the SAPISIDHASH is bound to (the app's real origin). */
14
+ export const VOICE_ORIGIN = "https://voice.google.com";
15
+ /** Host that actually serves the API (and that XD3 wants the Origin to match). */
16
+ export const CLIENTS6_ORIGIN = "https://clients6.google.com";
17
+ /** Full request URL for a voiceclient method. */
18
+ export function voiceUrl(method) {
19
+ return `${VOICE_API_BASE}/${method}?alt=protojson&key=${VOICE_API_KEY}`;
20
+ }
21
+ /** Substring identifying the send-SMS response (used when driving the browser). */
22
+ export const SEND_SMS_PATH = "/voiceclient/api2thread/sendsms";
23
+ /**
24
+ * `sendsms` is gated by reCAPTCHA Enterprise (this site key) + a BotGuard token,
25
+ * both minted only by Google's in-browser anti-bot JS — so `send` can't go over
26
+ * pure HTTP and must drive the authenticated page instead.
27
+ */
28
+ export const RECAPTCHA_SITE_KEY = "6Lfv4cYpAAAAABTKDTZu0jpcgdQ5Ak4XxeUBqA7B";
29
+ export const RECAPTCHA_ACTION = "SEND_SMS";
30
+ /** Builds the in-app conversation URL for a given target. */
31
+ export function threadUrl(target, authUser = 0) {
32
+ return `https://voice.google.com/u/${authUser}/messages?itemId=${encodeURIComponent(toThreadId(target))}`;
33
+ }
34
+ /**
35
+ * Selects the cookies clients6.google.com actually receives: only those scoped
36
+ * to `google.com`, de-duplicated by name. Chrome may hold same-named cookies
37
+ * for sibling domains (e.g. `.youtube.com`) or stale duplicates; sending those
38
+ * alongside — or hashing the wrong `SAPISID` — yields `401 invalid credentials`.
39
+ */
40
+ export function selectGoogleCookies(cookies) {
41
+ const byName = new Map();
42
+ for (const c of cookies) {
43
+ const domain = (c.domain || "").replace(/^\./, "");
44
+ if (domain !== "google.com")
45
+ continue;
46
+ byName.set(c.name, c.value); // last write wins (deduped)
47
+ }
48
+ const header = [...byName].map(([name, value]) => `${name}=${value}`).join("; ");
49
+ return { header, byName };
50
+ }
51
+ /** Cookies whose SHA-1 digests make up the multi-part Authorization header. */
52
+ const SAPISID_HASHES = [
53
+ ["SAPISIDHASH", "SAPISID"],
54
+ ["SAPISID1PHASH", "__Secure-1PAPISID"],
55
+ ["SAPISID3PHASH", "__Secure-3PAPISID"],
56
+ ];
57
+ /**
58
+ * Builds the `Authorization` header Google's JS clients send to clients6:
59
+ * `SAPISIDHASH <ts>_<sha1(`<ts> <cookie> <origin>`)> SAPISID1PHASH … SAPISID3PHASH …`
60
+ * Every *APISID cookie present is hashed against the *voice.google.com* origin.
61
+ */
62
+ export function buildSapisidAuth(byName, timestampSeconds, origin = VOICE_ORIGIN) {
63
+ const parts = [];
64
+ for (const [label, cookieName] of SAPISID_HASHES) {
65
+ const cookie = byName.get(cookieName);
66
+ if (!cookie)
67
+ continue;
68
+ const digest = createHash("sha1").update(`${timestampSeconds} ${cookie} ${origin}`).digest("hex");
69
+ parts.push(`${label} ${timestampSeconds}_${digest}`);
70
+ }
71
+ if (parts.length === 0) {
72
+ throw new Error("No Google session cookies (SAPISID) found for google.com. Make sure Chrome is logged into voice.google.com " +
73
+ "(and that PROFILE_PATH / --profile points at that profile).");
74
+ }
75
+ return parts.join(" ");
76
+ }
77
+ /**
78
+ * Headers for a clients6 voiceclient call. The cross-domain ("XD3") trick:
79
+ * • `Origin` must equal the Host (clients6) — the app issues the XHR from
80
+ * gapi's proxy iframe served *from* clients6, so anything else trips
81
+ * `400 "Origin doesn't match Host for XD3"`.
82
+ * • The real caller identity rides in `X-Origin` (= voice.google.com), which
83
+ * is what the SAPISIDHASH is bound to — anything else trips `401`.
84
+ * Those two together are why a naive same-origin request can't work.
85
+ */
86
+ export function voiceHeaders(authorization, cookieHeader) {
87
+ return {
88
+ authorization,
89
+ "content-type": "application/json+protobuf",
90
+ origin: CLIENTS6_ORIGIN,
91
+ referer: `${CLIENTS6_ORIGIN}/static/proxy.html?usegapi=1`,
92
+ "x-origin": VOICE_ORIGIN,
93
+ "x-referer": VOICE_ORIGIN,
94
+ "x-goog-authuser": "0",
95
+ "x-requested-with": "XMLHttpRequest",
96
+ "x-javascript-user-agent": "google-api-javascript-client/1.1.0",
97
+ "x-goog-encode-response-if-executable": "base64",
98
+ Cookie: cookieHeader,
99
+ };
100
+ }
101
+ /**
102
+ * Normalizes a US-centric phone number to E.164 (`+1XXXXXXXXXX`). Already-prefixed
103
+ * numbers and short codes (e.g. "39769") are passed through untouched.
104
+ */
105
+ export function normalizeNumber(input) {
106
+ const trimmed = input.trim();
107
+ if (trimmed.startsWith("+"))
108
+ return trimmed;
109
+ const digits = trimmed.replace(/\D/g, "");
110
+ if (digits.length === 10)
111
+ return `+1${digits}`;
112
+ if (digits.length === 11 && digits.startsWith("1"))
113
+ return `+${digits}`;
114
+ return digits; // short code or already-bare id
115
+ }
116
+ /**
117
+ * Resolves a thread id from either a raw thread id (`t.+1718…`, `t.39769`) or a
118
+ * phone number / short code (which the web app maps to `t.<E.164-or-shortcode>`).
119
+ */
120
+ export function toThreadId(target) {
121
+ if (target.startsWith("t."))
122
+ return target;
123
+ return `t.${normalizeNumber(target)}`;
124
+ }
125
+ const DIRECTION = { 5: "incoming", 6: "outgoing" };
126
+ /** Decodes one message tuple from an `api2thread/list` thread. */
127
+ export function decodeMessage(m) {
128
+ return {
129
+ id: m[0],
130
+ timestamp: m[1] ? new Date(m[1]).toISOString() : "",
131
+ from: Array.isArray(m[3]) ? (m[3][0] ?? null) : null,
132
+ text: m[9] ?? null,
133
+ direction: DIRECTION[m[12]] ?? "unknown",
134
+ // Incoming messages carry a read flag at index 5 (1 = read, 0 = unread);
135
+ // outgoing messages are always considered read.
136
+ read: m[12] === 6 ? true : m[5] === 1,
137
+ };
138
+ }
139
+ /** Decodes one thread tuple, newest message first (as the API returns it). */
140
+ export function decodeThread(t) {
141
+ const messages = (Array.isArray(t[2]) ? t[2] : []).map(decodeMessage);
142
+ const contact = Array.isArray(t[8]) ? (t[8][0] ?? null) : (t[0]?.replace(/^t\./, "") ?? null);
143
+ const latestIncoming = messages.find((msg) => msg.direction === "incoming");
144
+ return {
145
+ threadId: t[0],
146
+ contact,
147
+ unread: latestIncoming ? !latestIncoming.read : false,
148
+ latestText: messages[0]?.text ?? null,
149
+ latestTimestamp: messages[0]?.timestamp ?? null,
150
+ messageCount: messages.length,
151
+ messages,
152
+ };
153
+ }
154
+ /** Decodes a full `api2thread/list` response body into threads. */
155
+ export function decodeThreadList(body) {
156
+ const threads = Array.isArray(body?.[0]) ? body[0] : [];
157
+ return threads.map(decodeThread);
158
+ }
159
+ /**
160
+ * Body for `api2thread/list`: `[folder, threadLimit, perThreadLimit, null, null, [null,1,1,1]]`.
161
+ * `folder` 2 is the SMS/text inbox the web app loads by default.
162
+ */
163
+ export function buildListBody(threadLimit, perThreadLimit) {
164
+ return [2, threadLimit, perThreadLimit, null, null, [null, 1, 1, 1]];
165
+ }
166
+ /**
167
+ * Body for `api2thread/sendsms`:
168
+ * `[null,null,null,null, text, threadId, null, null, [transactionId]]`
169
+ * `transactionId` is a client idempotency id; any large positive integer works.
170
+ */
171
+ export function buildSendBody(text, threadId, transactionId) {
172
+ return [null, null, null, null, text, threadId, null, null, [transactionId]];
173
+ }
174
+ /** Decodes an `api2thread/sendsms` response into the sent-message summary. */
175
+ export function decodeSendResult(body) {
176
+ return {
177
+ threadId: body?.[1] ?? null,
178
+ messageId: body?.[2] ?? null,
179
+ timestamp: body?.[3] ? new Date(body[3]).toISOString() : null,
180
+ };
181
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: import("../../types.js").Site;
2
+ export default _default;