automate-google-login-scraper 0.1.9

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.
@@ -0,0 +1,276 @@
1
+ const SESSION_COOKIE_EXPIRES = -1;
2
+ const SAME_SITE = ["Strict", "Lax", "None"];
3
+ const GOOGLE_DOMAINS = ["google.com", "accounts.google.com", "googleusercontent.com", "gstatic.com"];
4
+ class InvalidStorageStateError extends Error {
5
+ constructor(reason) {
6
+ super(`Not a Playwright storage state: ${reason}`);
7
+ this.name = "InvalidStorageStateError";
8
+ }
9
+ }
10
+ function asSameSite(value) {
11
+ if (typeof value === "string") {
12
+ const match = SAME_SITE.find((candidate) => candidate.toLowerCase() === value.toLowerCase());
13
+ if (match) return match;
14
+ }
15
+ return "Lax";
16
+ }
17
+ function parseCookie(raw, index) {
18
+ if (typeof raw !== "object" || raw === null) {
19
+ throw new InvalidStorageStateError(`cookies[${index}] is not an object`);
20
+ }
21
+ const cookie = raw;
22
+ if (typeof cookie.name !== "string" || typeof cookie.value !== "string") {
23
+ throw new InvalidStorageStateError(`cookies[${index}] has no name/value pair`);
24
+ }
25
+ if (typeof cookie.domain !== "string" || cookie.domain === "") {
26
+ throw new InvalidStorageStateError(`cookies[${index}] (${cookie.name}) has no domain`);
27
+ }
28
+ return {
29
+ name: cookie.name,
30
+ value: cookie.value,
31
+ domain: cookie.domain,
32
+ path: typeof cookie.path === "string" ? cookie.path : "/",
33
+ expires: typeof cookie.expires === "number" ? cookie.expires : SESSION_COOKIE_EXPIRES,
34
+ httpOnly: cookie.httpOnly === true,
35
+ secure: cookie.secure === true,
36
+ sameSite: asSameSite(cookie.sameSite)
37
+ };
38
+ }
39
+ function parseOrigin(raw, index) {
40
+ if (typeof raw !== "object" || raw === null) {
41
+ throw new InvalidStorageStateError(`origins[${index}] is not an object`);
42
+ }
43
+ const origin = raw;
44
+ if (typeof origin.origin !== "string" || origin.origin === "") {
45
+ throw new InvalidStorageStateError(`origins[${index}] has no origin`);
46
+ }
47
+ const entries = Array.isArray(origin.localStorage) ? origin.localStorage : [];
48
+ return {
49
+ origin: origin.origin,
50
+ localStorage: entries.flatMap((entry) => {
51
+ if (typeof entry !== "object" || entry === null) return [];
52
+ const { name, value } = entry;
53
+ if (typeof name !== "string") return [];
54
+ return [{ name, value: typeof value === "string" ? value : String(value ?? "") }];
55
+ })
56
+ };
57
+ }
58
+ function normalizeStorageState(raw) {
59
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
60
+ throw new InvalidStorageStateError("top level is not an object");
61
+ }
62
+ const state = raw;
63
+ const cookies = state.cookies;
64
+ const origins = state.origins;
65
+ if (cookies !== void 0 && !Array.isArray(cookies)) {
66
+ throw new InvalidStorageStateError("`cookies` is present but not an array");
67
+ }
68
+ if (origins !== void 0 && !Array.isArray(origins)) {
69
+ throw new InvalidStorageStateError("`origins` is present but not an array");
70
+ }
71
+ if (cookies === void 0 && origins === void 0) {
72
+ throw new InvalidStorageStateError("neither `cookies` nor `origins` is present");
73
+ }
74
+ return {
75
+ cookies: (cookies ?? []).map(parseCookie),
76
+ origins: (origins ?? []).map(parseOrigin)
77
+ };
78
+ }
79
+ function parseStorageState(text) {
80
+ let parsed;
81
+ try {
82
+ parsed = JSON.parse(text);
83
+ } catch (error) {
84
+ throw new InvalidStorageStateError(`not valid JSON (${error.message})`);
85
+ }
86
+ return normalizeStorageState(parsed);
87
+ }
88
+ function isGoogleDomain(domain) {
89
+ const bare = domain.replace(/^\./, "").toLowerCase();
90
+ return GOOGLE_DOMAINS.some((google) => bare === google || bare.endsWith(`.${google}`));
91
+ }
92
+ function expiringCookies(state) {
93
+ return state.cookies.filter((cookie) => cookie.expires > 0);
94
+ }
95
+ function summarizeStorageState(state, options = {}) {
96
+ const nowSeconds = Math.floor((options.now ?? Date.now()) / 1e3);
97
+ const expiring = expiringCookies(state);
98
+ const earliestExpiry = expiring.length > 0 ? Math.min(...expiring.map((cookie) => cookie.expires)) : null;
99
+ const sessionCookies = state.cookies.length - expiring.length;
100
+ const expired = expiring.length > 0 && expiring.every((cookie) => cookie.expires <= nowSeconds);
101
+ const live = state.cookies.filter(
102
+ (cookie) => cookie.expires === SESSION_COOKIE_EXPIRES || cookie.expires > nowSeconds
103
+ );
104
+ return {
105
+ cookieCount: state.cookies.length,
106
+ originCount: state.origins.length,
107
+ domains: [...new Set(state.cookies.map((cookie) => cookie.domain))].sort(),
108
+ cookieNames: [...new Set(state.cookies.map((cookie) => cookie.name))].sort(),
109
+ localStorageKeys: Object.fromEntries(
110
+ state.origins.map((origin) => [origin.origin, origin.localStorage.map((entry) => entry.name).sort()])
111
+ ),
112
+ sessionCookies,
113
+ earliestExpiry,
114
+ expiresInSeconds: earliestExpiry === null ? null : earliestExpiry - nowSeconds,
115
+ expired,
116
+ usable: live.length > 0,
117
+ hasGoogleCookies: state.cookies.some((cookie) => isGoogleDomain(cookie.domain))
118
+ };
119
+ }
120
+ function findExpiringCookies(state, options = {}) {
121
+ const nowSeconds = Math.floor((options.now ?? Date.now()) / 1e3);
122
+ const withinSeconds = Math.floor((options.withinMs ?? 60 * 60 * 1e3) / 1e3);
123
+ return expiringCookies(state).filter((cookie) => cookie.expires - nowSeconds <= withinSeconds);
124
+ }
125
+ function redactStorageState(state) {
126
+ const redact = (value) => `«redacted ${value.length} chars»`;
127
+ return {
128
+ cookies: state.cookies.map((cookie) => ({ ...cookie, value: redact(cookie.value) })),
129
+ origins: state.origins.map((origin) => ({
130
+ origin: origin.origin,
131
+ localStorage: origin.localStorage.map((entry) => ({ name: entry.name, value: redact(entry.value) }))
132
+ }))
133
+ };
134
+ }
135
+ function toPuppeteerCookies(state) {
136
+ return state.cookies.map((cookie) => {
137
+ const converted = {
138
+ name: cookie.name,
139
+ value: cookie.value,
140
+ domain: cookie.domain,
141
+ path: cookie.path,
142
+ httpOnly: cookie.httpOnly,
143
+ secure: cookie.secure,
144
+ sameSite: cookie.sameSite
145
+ };
146
+ if (cookie.expires > 0) converted.expires = cookie.expires;
147
+ return converted;
148
+ });
149
+ }
150
+ function fromPuppeteerCookies(cookies) {
151
+ return cookies.map((cookie) => {
152
+ const hasExpiry = cookie.session !== true && typeof cookie.expires === "number" && cookie.expires > 0;
153
+ const sameSite = ["Strict", "Lax", "None"].find(
154
+ (candidate) => candidate.toLowerCase() === String(cookie.sameSite ?? "").toLowerCase()
155
+ );
156
+ return {
157
+ name: cookie.name,
158
+ value: cookie.value,
159
+ domain: cookie.domain,
160
+ path: cookie.path || "/",
161
+ expires: hasExpiry ? cookie.expires : SESSION_COOKIE_EXPIRES,
162
+ httpOnly: cookie.httpOnly === true,
163
+ secure: cookie.secure === true,
164
+ sameSite: sameSite ?? "Lax"
165
+ };
166
+ });
167
+ }
168
+ function storageStateOrigins(state) {
169
+ return state.origins.map((origin) => origin.origin);
170
+ }
171
+ async function applyLocalStorage(page, entries) {
172
+ if (entries.length === 0) return;
173
+ await page.evaluate(
174
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- runs in the page, not here
175
+ ((items) => {
176
+ for (const item of items) window.localStorage.setItem(item.name, item.value);
177
+ }),
178
+ entries
179
+ );
180
+ }
181
+ async function applyStorageState(page, state, options = {}) {
182
+ const result = { cookiesSet: 0, originsRestored: [], skipped: [] };
183
+ const cookies = toPuppeteerCookies(state);
184
+ if (cookies.length > 0) {
185
+ if (typeof page.setCookie !== "function") {
186
+ throw new TypeError(
187
+ "This page cannot set cookies — `page.setCookie` is missing. Puppeteer 23+ moved it to `browserContext.setCookie`; pass a page from an older API, or set them on the context yourself."
188
+ );
189
+ }
190
+ await page.setCookie(...cookies);
191
+ result.cookiesSet = cookies.length;
192
+ }
193
+ const wanted = options.origins;
194
+ const origins = wanted ? state.origins.filter((origin) => wanted.includes(origin.origin)) : state.origins;
195
+ for (const origin of origins) {
196
+ if (origin.localStorage.length === 0) {
197
+ result.skipped.push({ origin: origin.origin, reason: "no localStorage entries" });
198
+ continue;
199
+ }
200
+ try {
201
+ await page.goto(origin.origin, { waitUntil: "domcontentloaded", ...options.gotoOptions });
202
+ await applyLocalStorage(page, origin.localStorage);
203
+ result.originsRestored.push(origin.origin);
204
+ } catch (error) {
205
+ result.skipped.push({ origin: origin.origin, reason: error.message });
206
+ }
207
+ }
208
+ return result;
209
+ }
210
+ async function extractStorageState(page, options = {}) {
211
+ if (typeof page.cookies !== "function") {
212
+ throw new TypeError("This page cannot read cookies — `page.cookies` is missing.");
213
+ }
214
+ const cookies = fromPuppeteerCookies(await page.cookies());
215
+ const current = page.url();
216
+ const origins = options.origins ?? (current && current !== "about:blank" ? [originOf(current)] : []);
217
+ const captured = [];
218
+ for (const origin of origins) {
219
+ try {
220
+ if (originOf(page.url()) !== origin) {
221
+ await page.goto(origin, { waitUntil: "domcontentloaded", ...options.gotoOptions });
222
+ }
223
+ const localStorage = await page.evaluate(
224
+ (() => Object.keys(window.localStorage).map((name) => ({
225
+ name,
226
+ value: window.localStorage.getItem(name) ?? ""
227
+ })))
228
+ );
229
+ captured.push({ origin, localStorage });
230
+ } catch {
231
+ }
232
+ }
233
+ return { cookies, origins: captured };
234
+ }
235
+ function originOf(url) {
236
+ try {
237
+ return new URL(url).origin;
238
+ } catch {
239
+ return url;
240
+ }
241
+ }
242
+ const encoder = new TextEncoder();
243
+ function constantTimeEqual(supplied, expected) {
244
+ if (!expected) return false;
245
+ if (supplied == null) return false;
246
+ const a = encoder.encode(supplied);
247
+ const b = encoder.encode(expected);
248
+ let diff = a.length ^ b.length;
249
+ for (let i = 0; i < b.length; i += 1) {
250
+ diff |= (a[i] ?? 65535) ^ b[i];
251
+ }
252
+ return diff === 0;
253
+ }
254
+ const AUTH_HEADER = "x-test-auth-secret";
255
+ function isAuthorized(headers, expected) {
256
+ return constantTimeEqual(headers.get(AUTH_HEADER), expected);
257
+ }
258
+ export {
259
+ AUTH_HEADER as A,
260
+ InvalidStorageStateError as I,
261
+ SESSION_COOKIE_EXPIRES as S,
262
+ applyLocalStorage as a,
263
+ applyStorageState as b,
264
+ constantTimeEqual as c,
265
+ fromPuppeteerCookies as d,
266
+ extractStorageState as e,
267
+ findExpiringCookies as f,
268
+ isGoogleDomain as g,
269
+ storageStateOrigins as h,
270
+ isAuthorized as i,
271
+ normalizeStorageState as n,
272
+ parseStorageState as p,
273
+ redactStorageState as r,
274
+ summarizeStorageState as s,
275
+ toPuppeteerCookies as t
276
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Shared-secret comparison, in a form that runs in both halves of this package.
3
+ *
4
+ * `node:crypto.timingSafeEqual` would do, and the Worker could reach it through
5
+ * `nodejs_compat` — but this is twenty lines, needs no compatibility flag, and
6
+ * the Worker half is deliberately free of Node builtins so its bundle stays a
7
+ * Worker bundle. Hence the hand-rolled compare.
8
+ */
9
+ /**
10
+ * Compare two secrets without leaking their contents through timing.
11
+ *
12
+ * A length mismatch is answered in the same shape as a content mismatch — the
13
+ * loop always runs over the expected value's full length — so an attacker
14
+ * learns neither the length nor the position of the first wrong byte.
15
+ *
16
+ * @param supplied what the caller presented (`null`/`undefined` is a miss, not a throw)
17
+ * @param expected the configured secret (empty or unset is always a miss)
18
+ */
19
+ export declare function constantTimeEqual(supplied: string | null | undefined, expected: string | null | undefined): boolean;
20
+ /** Header every request to the Worker half must carry. */
21
+ export declare const AUTH_HEADER = "x-test-auth-secret";
22
+ /**
23
+ * Whether a request presented the configured secret.
24
+ *
25
+ * Returns `false` when the secret is not configured at all, so a Worker deployed
26
+ * without `TEST_AUTH_SECRET` set refuses every request rather than serving an
27
+ * open browser to the internet.
28
+ */
29
+ export declare function isAuthorized(headers: {
30
+ get(name: string): string | null;
31
+ }, expected: string | null | undefined): boolean;
@@ -0,0 +1,50 @@
1
+ import { StorageState, StorageStateCookie, StorageStateSummary } from './types.js';
2
+ /** Playwright's sentinel for "dies with the browser". Not a timestamp. */
3
+ export declare const SESSION_COOKIE_EXPIRES = -1;
4
+ /** Thrown when a state file exists but is not a storage state. */
5
+ export declare class InvalidStorageStateError extends Error {
6
+ constructor(reason: string);
7
+ }
8
+ /**
9
+ * Validate and normalize a parsed storage state.
10
+ *
11
+ * Normalizing rather than trusting: a state file may have been written by an
12
+ * older Playwright, hand-edited, or produced by the Puppeteer half of this
13
+ * package, and a missing `path` or a lowercase `sameSite` should not fail a
14
+ * suite three calls later with an opaque CDP error.
15
+ */
16
+ export declare function normalizeStorageState(raw: unknown): StorageState;
17
+ /** Parse JSON text into a validated storage state. */
18
+ export declare function parseStorageState(text: string): StorageState;
19
+ /** Is this domain Google's, rather than the app under test's? */
20
+ export declare function isGoogleDomain(domain: string): boolean;
21
+ /**
22
+ * Everything worth knowing about a state file, with no value from it included.
23
+ *
24
+ * `now` is injectable because every assertion about expiry otherwise depends on
25
+ * the wall clock, which makes for a test suite that passes until it doesn't.
26
+ *
27
+ * @param now milliseconds since epoch. Defaults to `Date.now()`.
28
+ */
29
+ export declare function summarizeStorageState(state: StorageState, options?: {
30
+ now?: number;
31
+ }): StorageStateSummary;
32
+ /**
33
+ * Cookies that expire within `withinMs` — the warning before the failure.
34
+ *
35
+ * Worth running at the start of a suite: "3 cookies expire in 40 minutes" in the
36
+ * log beats a flaky-looking selector timeout at minute 41.
37
+ */
38
+ export declare function findExpiringCookies(state: StorageState, options?: {
39
+ now?: number;
40
+ withinMs?: number;
41
+ }): StorageStateCookie[];
42
+ /**
43
+ * The same state with every secret replaced by a description of its length.
44
+ *
45
+ * Shape, names, domains, flags and expiries survive; cookie values and
46
+ * `localStorage` values do not. This is the only form of a state file that is
47
+ * safe to print, log, attach to an issue or return over HTTP — and having it
48
+ * means the answer to "can I see the state file?" is yes, this one.
49
+ */
50
+ export declare function redactStorageState(state: StorageState): StorageState;
@@ -0,0 +1,50 @@
1
+ import { StorageState, StorageStateSummary } from './types.js';
2
+ export * from './state-core.js';
3
+ /** Thrown when a state file is absent — carries the command that creates one. */
4
+ export declare class MissingStorageStateError extends Error {
5
+ readonly file: string;
6
+ constructor(file: string, hint: string);
7
+ }
8
+ /**
9
+ * Read a state file from disk.
10
+ *
11
+ * @throws {MissingStorageStateError} when the file is absent, with the codegen
12
+ * command that creates it — the message is the whole fix, so it belongs in the
13
+ * error rather than in documentation the reader has to go find.
14
+ */
15
+ export declare function readStorageState(file: string, options?: {
16
+ baseUrl?: string;
17
+ cwd?: string;
18
+ }): StorageState;
19
+ /**
20
+ * Write a state file, owner-read-write only.
21
+ *
22
+ * The `chmod` after the write is not redundant: `writeFileSync`'s `mode` applies
23
+ * only when it creates the file, so re-saving over an existing 0644 state file
24
+ * would otherwise keep the loose mode it already had.
25
+ */
26
+ export declare function writeStorageState(file: string, state: StorageState): string;
27
+ /**
28
+ * Tighten an existing state file to 0600 and confirm it parses.
29
+ *
30
+ * For the files Playwright writes itself. `context.storageState({ path })` keeps
31
+ * shapes this package has no type for — `indexedDB: true` adds one — so
32
+ * round-tripping such a file through {@link writeStorageState} would quietly drop
33
+ * data the app needs. This validates and chmods in place instead.
34
+ *
35
+ * @returns a summary of what the file holds, safe to log
36
+ * @throws {InvalidStorageStateError} if the file is not a storage state after all
37
+ */
38
+ export declare function hardenStorageStateFile(file: string): StorageStateSummary;
39
+ /**
40
+ * Throw unless the state is still usable, with a message that says what to do.
41
+ *
42
+ * This is what a Playwright setup project calls: it turns "the dashboard heading
43
+ * never appeared" into "the session expired 6 hours ago, re-capture it like this".
44
+ */
45
+ export declare function assertStorageStateUsable(state: StorageState, options?: {
46
+ now?: number;
47
+ file?: string;
48
+ baseUrl?: string;
49
+ cwd?: string;
50
+ }): StorageStateSummary;
@@ -0,0 +1,99 @@
1
+ /**
2
+ * The shapes this package works in.
3
+ *
4
+ * `StorageState` is Playwright's `storageState()` output, declared here rather
5
+ * than imported from `@playwright/test` on purpose: the Worker half of this
6
+ * package reads the same file inside a Cloudflare isolate, where Playwright is
7
+ * not installed and cannot be. Keeping the type local is what lets one state
8
+ * file be produced by Playwright and consumed by Puppeteer.
9
+ */
10
+ /** Playwright's `sameSite` spelling. Puppeteer/CDP uses the same three words. */
11
+ export type SameSite = "Strict" | "Lax" | "None";
12
+ /** One cookie as Playwright writes it into a storage-state file. */
13
+ export interface StorageStateCookie {
14
+ name: string;
15
+ value: string;
16
+ domain: string;
17
+ path: string;
18
+ /**
19
+ * Unix seconds, **not** milliseconds — and `-1` for a session cookie, which is
20
+ * the single most common way to get expiry arithmetic wrong here.
21
+ */
22
+ expires: number;
23
+ httpOnly: boolean;
24
+ secure: boolean;
25
+ sameSite: SameSite;
26
+ }
27
+ /** `localStorage` for one origin. Playwright groups storage by origin, not by domain. */
28
+ export interface StorageStateOrigin {
29
+ origin: string;
30
+ localStorage: {
31
+ name: string;
32
+ value: string;
33
+ }[];
34
+ }
35
+ /**
36
+ * A whole persisted browser session: cookies plus per-origin `localStorage`.
37
+ *
38
+ * **This is a credential.** A populated instance of this type grants whatever the
39
+ * signed-in test account can do, which is why nothing in this package logs one,
40
+ * returns one over HTTP, or writes one anywhere but a 0600 file under
41
+ * `playwright/.auth/`.
42
+ */
43
+ export interface StorageState {
44
+ cookies: StorageStateCookie[];
45
+ origins: StorageStateOrigin[];
46
+ }
47
+ /** What a cookie looks like to Puppeteer's `setCookie`/`cookies` (CDP naming). */
48
+ export interface PuppeteerCookie {
49
+ name: string;
50
+ value: string;
51
+ domain: string;
52
+ path: string;
53
+ /** Unix seconds. Absent (not `-1`) for a session cookie. */
54
+ expires?: number;
55
+ httpOnly: boolean;
56
+ secure: boolean;
57
+ sameSite?: SameSite;
58
+ /** CDP sets this on read; it is the inverse of "has an expiry". */
59
+ session?: boolean;
60
+ }
61
+ /**
62
+ * What `summarizeStorageState` reports — everything you would want to know about
63
+ * a state file without ever seeing a value from it. Safe to print and to log.
64
+ */
65
+ export interface StorageStateSummary {
66
+ cookieCount: number;
67
+ originCount: number;
68
+ /** Cookie domains, deduped and sorted. Names and values are never included. */
69
+ domains: string[];
70
+ /** Cookie names, deduped and sorted — a name is not a secret, a value is. */
71
+ cookieNames: string[];
72
+ /** `localStorage` keys per origin, values omitted. */
73
+ localStorageKeys: Record<string, string[]>;
74
+ /** How many cookies carry no expiry and die with the browser. */
75
+ sessionCookies: number;
76
+ /** Earliest expiry among cookies that have one, as Unix seconds; `null` if none do. */
77
+ earliestExpiry: number | null;
78
+ /** Seconds until `earliestExpiry`; negative once it has passed. `null` if none do. */
79
+ expiresInSeconds: number | null;
80
+ /** True when every cookie that had an expiry has passed it. */
81
+ expired: boolean;
82
+ /** True when the state still has at least one cookie a browser would send. */
83
+ usable: boolean;
84
+ /** True when any cookie belongs to a Google domain. */
85
+ hasGoogleCookies: boolean;
86
+ }
87
+ /** The minimum of a Puppeteer/Playwright `Page` that this package calls. */
88
+ export interface PageLike {
89
+ goto(url: string, options?: Record<string, unknown>): Promise<unknown>;
90
+ evaluate<T>(fn: (...args: never[]) => T, ...args: unknown[]): Promise<T>;
91
+ url(): string;
92
+ setCookie?(...cookies: PuppeteerCookie[]): Promise<void>;
93
+ cookies?(...urls: string[]): Promise<PuppeteerCookie[]>;
94
+ deleteCookie?(...cookies: {
95
+ name: string;
96
+ domain?: string;
97
+ path?: string;
98
+ }[]): Promise<void>;
99
+ }
@@ -0,0 +1,138 @@
1
+ import { PageLike, StorageStateSummary } from './types.js';
2
+ /** Bindings this Worker expects. See `wrangler.jsonc`. */
3
+ export interface Env {
4
+ /** Browser Rendering binding. Requires the Workers Paid plan. */
5
+ BROWSER: unknown;
6
+ /** The `GoogleLoginBrowser` namespace. */
7
+ BROWSER_SESSION: DurableObjectNamespaceLike;
8
+ /** Shared secret every request must present. Unset = the Worker serves nothing. */
9
+ TEST_AUTH_SECRET?: string;
10
+ /** "true" to enable `POST /login`. Anything else leaves it disabled. */
11
+ ALLOW_REAL_GOOGLE_LOGIN?: string;
12
+ GOOGLE_TEST_EMAIL?: string;
13
+ GOOGLE_TEST_PASSWORD?: string;
14
+ /** Minutes of inactivity before the browser is closed. Default 3. */
15
+ BROWSER_IDLE_MINUTES?: string;
16
+ }
17
+ /** The shape of a DO namespace, declared structurally so tests can fake it. */
18
+ export interface DurableObjectNamespaceLike {
19
+ idFromName(name: string): unknown;
20
+ get(id: unknown): {
21
+ fetch(request: Request): Promise<Response>;
22
+ };
23
+ }
24
+ /**
25
+ * Route a request to the DO that owns its session, after checking the secret.
26
+ *
27
+ * The session name comes from `?session=` and defaults to `default`. Distinct
28
+ * names are distinct browsers — useful for one signed-in and one anonymous
29
+ * session in the same suite, and for keeping two concurrent CI jobs apart.
30
+ */
31
+ export declare function handleRequest(request: Request, env: Env): Promise<Response>;
32
+ declare const _default: {
33
+ fetch: typeof handleRequest;
34
+ };
35
+ export default _default;
36
+ /** The minimum of `@cloudflare/puppeteer` this module calls. */
37
+ export interface PuppeteerLike {
38
+ launch(binding: unknown): Promise<BrowserLike>;
39
+ connect?(binding: unknown, sessionId: string): Promise<BrowserLike>;
40
+ }
41
+ export interface BrowserLike {
42
+ newPage(): Promise<PageLike & {
43
+ close(): Promise<void>;
44
+ title(): Promise<string>;
45
+ screenshot(options?: Record<string, unknown>): Promise<ArrayBuffer | Uint8Array>;
46
+ }>;
47
+ close(): Promise<void>;
48
+ isConnected?(): boolean;
49
+ }
50
+ /** Inject a Puppeteer implementation — for tests, and for bring-your-own builds. */
51
+ export declare function setPuppeteer(implementation: PuppeteerLike | undefined): void;
52
+ /** State-checking result, returned by `POST /check`. */
53
+ export interface CheckResult {
54
+ authenticated: boolean;
55
+ status: number | null;
56
+ url: string;
57
+ title: string;
58
+ /** Why the check concluded what it did — the useful half when it says `false`. */
59
+ reason: string;
60
+ applied: {
61
+ cookiesSet: number;
62
+ originsRestored: string[];
63
+ skipped: {
64
+ origin: string;
65
+ reason: string;
66
+ }[];
67
+ };
68
+ }
69
+ /**
70
+ * Holds one browser and one persisted session.
71
+ *
72
+ * Written against a structural `state`/`env` rather than extending
73
+ * `DurableObject`, so the class is constructible in a plain Vitest process. The
74
+ * runtime passes the real ones.
75
+ */
76
+ export declare class GoogleLoginBrowser {
77
+ private readonly storage;
78
+ private readonly env;
79
+ private browser;
80
+ constructor(state: {
81
+ storage: DurableObjectStorageLike;
82
+ }, env: Env);
83
+ private get idleMs();
84
+ fetch(request: Request): Promise<Response>;
85
+ /**
86
+ * Store a storage state for this session.
87
+ *
88
+ * Validated on the way in, so a malformed upload fails here with a precise
89
+ * reason rather than as an opaque CDP error during the next check.
90
+ */
91
+ private putState;
92
+ /**
93
+ * Describe the stored state — redacted, always.
94
+ *
95
+ * There is no route that returns the live values. That is the point: this Worker
96
+ * can be handed to CI without handing CI a way to read the session back out.
97
+ */
98
+ private describeState;
99
+ private clearState;
100
+ /**
101
+ * Replay the stored session against a URL and report whether it is signed in.
102
+ *
103
+ * "Signed in" is whatever you say it is: `expectSelector` is checked in the page
104
+ * and `rejectUrl` catches the redirect-to-login case, which is the failure that
105
+ * otherwise looks like a passing 200.
106
+ */
107
+ private check;
108
+ /**
109
+ * Sign in to Google for real, in the Worker, and store the resulting session.
110
+ *
111
+ * Gated twice — the flag and the credentials — and still the path you should
112
+ * reach for last. Read {@link signInWithGoogle}'s notes in `google-login.ts`:
113
+ * they apply here in full, with the extra wrinkle that a datacentre IP makes
114
+ * Google's risk checks more likely, not less.
115
+ */
116
+ private login;
117
+ private ensureBrowser;
118
+ /** Push the idle alarm out. Called after every request that used the browser. */
119
+ private touch;
120
+ /**
121
+ * Close the browser when the session goes quiet.
122
+ *
123
+ * Not an optimisation — a Browser Rendering session left open is billed and
124
+ * counts against the account's concurrency limit, so the alarm is the difference
125
+ * between a test tool and a surprise invoice.
126
+ */
127
+ alarm(): Promise<void>;
128
+ private closeBrowser;
129
+ }
130
+ /** The part of a DO's storage this class uses. */
131
+ export interface DurableObjectStorageLike {
132
+ get<T>(key: string): Promise<T | undefined>;
133
+ put(key: string, value: unknown): Promise<void>;
134
+ delete(key: string): Promise<boolean>;
135
+ setAlarm(time: number): Promise<void>;
136
+ }
137
+ /** Exported for tests and for the summary type. */
138
+ export type { StorageStateSummary };