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.
package/README.md ADDED
@@ -0,0 +1,378 @@
1
+ <!-- template-git-repo:badges:start -->
2
+ <p align="center">
3
+ <a href="https://starterdocs.vtempest.workers.dev/docs/packages/test-google-login"><img src="https://img.shields.io/badge/Docs-blue?logo=ReadTheDocs&logoColor=white" alt="Documentation" /></a>
4
+ <br />
5
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/stargazers"><img src="https://img.shields.io/github/stars/OpenSourceAGI/dev-tools-starter-agent" alt="GitHub Stars" /></a>
6
+ <a href="https://www.npmjs.com/package/test-google-login"><img src="https://img.shields.io/npm/dm/test-google-login.svg" alt="NPM Monthly Downloads" /></a>
7
+ <a href="https://www.npmjs.com/package/test-google-login"><img src="https://img.shields.io/npm/v/test-google-login.svg" alt="npm version" /></a>
8
+ <a href="https://www.npmjs.com/package/test-google-login"><img src="https://img.shields.io/npm/dt/test-google-login.svg" alt="NPM Total Downloads" /></a>
9
+ <a href="https://www.npmjs.com/package/test-google-login"><img src="https://img.shields.io/npm/types/test-google-login" alt="TypeScript types" /></a>
10
+ <a href="https://packagephobia.com/result?p=test-google-login"><img src="https://packagephobia.com/badge?p=test-google-login" alt="Install size" /></a>
11
+ <a href="https://app.codecov.io/gh/OpenSourceAGI/dev-tools-starter-agent/flags"><img src="https://img.shields.io/codecov/c/github/OpenSourceAGI/dev-tools-starter-agent?flag=test-google-login&label=test-google-login%20coverage&logo=codecov&logoColor=white" alt="Coverage" /></a>
12
+ <br />
13
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/issues"><img src="https://img.shields.io/github/issues/OpenSourceAGI/dev-tools-starter-agent?logo=github" alt="GitHub Issues" /></a>
14
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/pulls"><img src="https://img.shields.io/github/issues-pr/OpenSourceAGI/dev-tools-starter-agent?logo=github&label=PRs" alt="Open Pull Requests" /></a>
15
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/pulls?q=is%3Apr+is%3Aclosed"><img src="https://img.shields.io/github/issues-pr-closed/OpenSourceAGI/dev-tools-starter-agent?logo=github&label=PRs%20merged&color=8957e5" alt="Merged Pull Requests" /></a>
16
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/discussions"><img src="https://img.shields.io/github/discussions/OpenSourceAGI/dev-tools-starter-agent" alt="GitHub Discussions" /></a>
17
+ <a href="https://github.com/OpenSourceAGI/dev-tools-starter-agent/commits/master/"><img src="https://img.shields.io/github/last-commit/OpenSourceAGI/dev-tools-starter-agent.svg" alt="GitHub last commit" /></a>
18
+ <br />
19
+ <a href="https://stackblitz.com/github/OpenSourceAGI/dev-tools-starter-agent/tree/master/packages/test-google-login"><img height="20px" src="https://developer.stackblitz.com/img/open_in_stackblitz.svg" alt="Open in StackBlitz" /></a>
20
+ <img src="https://img.shields.io/badge/Bun-14151A?logo=bun&logoColor=white" alt="Bun" /> <img src="https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white" alt="TypeScript" /> <img src="https://img.shields.io/badge/Cloudflare%20Workers-F38020?logo=cloudflareworkers&logoColor=white" alt="Cloudflare Workers" /> <img src="https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=white" alt="Vite" /> <img src="https://img.shields.io/badge/Vitest-6E9F18?logo=vitest&logoColor=white" alt="Vitest" /> <img src="https://img.shields.io/badge/Playwright-2EAD33?logo=playwright&logoColor=white" alt="Playwright" />
21
+ </p>
22
+ <!-- template-git-repo:badges:end -->
23
+
24
+ <!-- skills:install:start -->
25
+ **🤖 Agent skill** — `npx skills@latest add https://github.com/OpenSourceAGI/dev-tools-starter-agent --skill test-google-login` ([what it covers](../../skills/test-google-login/SKILL.md))
26
+ <!-- skills:install:end -->
27
+
28
+ # Test Google Login
29
+
30
+ Sign in with Google **once**, by hand, and let every test after that start
31
+ already signed in — locally, in CI, and from a Cloudflare Worker.
32
+
33
+ Two halves, one session file:
34
+
35
+ - **Playwright** (`test-google-login`) — captures, validates, inspects and
36
+ redacts a `storageState`, and guards a setup project so the first run tells a
37
+ new contributor exactly what to do instead of timing out on a login form.
38
+ - **Cloudflare Workers** (`test-google-login/worker`) — a Browser Rendering
39
+ Durable Object that replays the same session against a deployed app, keeping
40
+ one browser alive across a whole suite.
41
+
42
+ It has **no runtime dependencies**. Playwright and `@cloudflare/puppeteer` are
43
+ optional peers: every function takes the `page`, `context` or `request` it needs
44
+ as an argument, which is also why the whole package is unit-tested without ever
45
+ launching a browser.
46
+
47
+ ## ⚠ The security boundary — read this first
48
+
49
+ Putting `GOOGLE_TEST_PASSWORD` in `.env` protects nothing if you then write:
50
+
51
+ ```text
52
+ playwright/.auth/google-test-user.json
53
+ ```
54
+
55
+ **That file is a live credential.** It can contain your app's session cookie,
56
+ Google's cookies for `accounts.google.com`, OAuth tokens in `localStorage` or
57
+ IndexedDB, and — depending on your auth library — something refresh-capable.
58
+ Anyone holding it is signed in as the test account.
59
+
60
+ So:
61
+
62
+ - Use a **dedicated Google account** for testing, with nothing sensitive on it.
63
+ - **Never commit it.** `test-google-login init` adds the rules, twice.
64
+ - **Never upload it as a public CI artifact**, and never paste it into a log,
65
+ an issue or a chat. Use `test-google-login redact` when you need to show it.
66
+ - **Regenerate it** when it stops working, rather than trying to repair it.
67
+ - Prefer testing **your app's own session**. Keep real Google sign-in as a
68
+ narrow, occasional smoke test.
69
+
70
+ This package is built around that last point. Everything that touches Google
71
+ is opt-in and off by default; everything that touches the file is 0600.
72
+
73
+ ## Install
74
+
75
+ ```bash
76
+ bun add -d test-google-login
77
+ npm install --save-dev test-google-login
78
+ ```
79
+
80
+ ## Quick start
81
+
82
+ ### 1. Set up the directory and the gitignore rules
83
+
84
+ ```bash
85
+ npx test-google-login init
86
+ ```
87
+
88
+ ```text
89
+ Created playwright/.auth/ (mode 0700)
90
+ Wrote playwright/.auth/.gitignore — ignores everything in that directory
91
+ Added to .gitignore: playwright/.auth/, .env, .env.*, !.env.example
92
+
93
+ Now capture a session by hand — this avoids automating Google's password form,
94
+ which is what MFA, CAPTCHA and device checks all break:
95
+
96
+ npx playwright codegen --save-storage=playwright/.auth/google-test-user.json http://localhost:3000
97
+ ```
98
+
99
+ Two gitignores on purpose: the root one, and `playwright/.auth/.gitignore`
100
+ containing `*`. The root file gets reverted, reformatted and replaced by tooling;
101
+ the nested one travels with the directory.
102
+
103
+ ### 2. Capture a session, by hand, once
104
+
105
+ Run the command `init` printed. In the window that opens: click **Sign in with
106
+ Google**, use the dedicated test account, finish the redirect back to *your* app,
107
+ confirm you are on an authenticated page, then close the window — Playwright
108
+ writes the state file on exit.
109
+
110
+ This is manual on purpose. Google may ask for MFA, a CAPTCHA or a device check,
111
+ and none of those should be automated around.
112
+
113
+ ### 3. Reuse it
114
+
115
+ `playwright.config.ts`:
116
+
117
+ ```ts
118
+ import { defineConfig, devices } from "@playwright/test";
119
+ import { DEFAULT_AUTH_FILE } from "test-google-login";
120
+
121
+ export default defineConfig({
122
+ testDir: "./tests/e2e",
123
+ use: { baseURL: process.env.E2E_BASE_URL ?? "http://localhost:3000" },
124
+ projects: [
125
+ { name: "auth-setup", testMatch: /.*\.setup\.ts/ },
126
+ {
127
+ name: "chromium",
128
+ dependencies: ["auth-setup"],
129
+ use: { ...devices["Desktop Chrome"], storageState: DEFAULT_AUTH_FILE },
130
+ },
131
+ ],
132
+ });
133
+ ```
134
+
135
+ `tests/auth.setup.ts`:
136
+
137
+ ```ts
138
+ import { test as setup } from "@playwright/test";
139
+ import { requireStoredSession } from "test-google-login";
140
+
141
+ setup("an authenticated session is available", () => {
142
+ const summary = requireStoredSession({ baseUrl: process.env.E2E_BASE_URL });
143
+ console.log(`session ok — ${summary.cookieCount} cookies, ${summary.domains.join(", ")}`);
144
+ });
145
+ ```
146
+
147
+ Your tests now start signed in:
148
+
149
+ ```ts
150
+ test("an authenticated user can open the dashboard", async ({ page }) => {
151
+ await page.goto("/dashboard");
152
+ await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible();
153
+ });
154
+ ```
155
+
156
+ `requireStoredSession` is the part that earns its keep. A missing file throws the
157
+ `codegen` command; an expired one throws *how long ago* it lapsed:
158
+
159
+ ```text
160
+ The persisted session is no longer usable — it expired 380 minutes ago.
161
+
162
+ Re-capture it:
163
+ npx playwright codegen --save-storage=playwright/.auth/google-test-user.json http://localhost:3000
164
+ ```
165
+
166
+ Without that, an expired session presents as a selector timeout on an
167
+ unauthenticated page, which reads like a broken test rather than a stale cookie.
168
+
169
+ ## For CI: let your app mint the session
170
+
171
+ Do **not** make a Google password a repository secret every fork's pull request
172
+ can reach. Instead, add a test-only endpoint to your own staging environment that
173
+ creates the same session a successful Google callback creates, and hold one secret
174
+ of yours.
175
+
176
+ ```ts
177
+ import { bootstrapAppSession } from "test-google-login";
178
+
179
+ setup("create authenticated test session", async ({ page, context, request }) => {
180
+ await bootstrapAppSession({
181
+ request,
182
+ page,
183
+ context,
184
+ secret: process.env.E2E_TEST_AUTH_SECRET, // required; never defaulted
185
+ endpoint: "/api/test-auth/google-user",
186
+ landingPath: "/dashboard",
187
+ });
188
+ });
189
+ ```
190
+
191
+ State is regenerated each run, no Google traffic is involved, and a PR check can
192
+ never fail because Google showed a consent screen. See
193
+ [`examples/test-auth-endpoint.ts`](examples/test-auth-endpoint.ts) for the
194
+ endpoint — it must 404 in production, require a strong secret compared in
195
+ constant time, and create a session identical to the real callback's.
196
+
197
+ ### Which strategy where
198
+
199
+ | Strategy | Use for | Secrets held |
200
+ | --- | --- | --- |
201
+ | `bootstrapAppSession` — your app mints it | every PR check | `E2E_TEST_AUTH_SECRET` |
202
+ | `requireStoredSession` — captured by hand | local development | none |
203
+ | Encrypted stored state | private, controlled CI | the encryption key; delete state after the run |
204
+ | `signInWithGoogle` — real Google | occasional manual smoke test | a dedicated account's credentials, environment-scoped |
205
+ | Mock the OAuth callback | unit and integration tests | none |
206
+
207
+ ## Inspecting a session without opening it
208
+
209
+ ```bash
210
+ npx test-google-login check
211
+ ```
212
+
213
+ ```text
214
+ State file playwright/.auth/google-test-user.json
215
+ Cookies 3 (1 session-only)
216
+ Domains .google.com, app.example.test
217
+ Origins 1
218
+ Google cookies present: yes
219
+ Expiry earliest in 718 minutes
220
+ ```
221
+
222
+ Exit codes are meant for CI: `0` healthy, `1` missing or expired, `2` present but
223
+ malformed. `--json` for machine-readable output, `--within 30` to warn about
224
+ cookies expiring inside a window.
225
+
226
+ ```bash
227
+ npx test-google-login redact # safe to paste into an issue
228
+ npx test-google-login clear # delete the session
229
+ ```
230
+
231
+ `redact` keeps the shape — names, domains, flags, expiries — and replaces every
232
+ value with `«redacted 21 chars»`. Nothing in this package ever prints a value:
233
+ `summarizeStorageState` returns names and domains only, and the Worker's
234
+ `GET /state` returns a redacted copy.
235
+
236
+ ## The Cloudflare Workers half
237
+
238
+ ```ts
239
+ import { GoogleLoginBrowser, handleRequest } from "test-google-login/worker";
240
+
241
+ export { GoogleLoginBrowser };
242
+ export default { fetch: handleRequest };
243
+ ```
244
+
245
+ ```bash
246
+ wrangler secret put TEST_AUTH_SECRET
247
+ wrangler deploy
248
+ ```
249
+
250
+ A Durable Object per named session, holding one Browser Rendering browser. Why a
251
+ DO rather than `puppeteer.launch()` in the Worker: a launch costs seconds and is
252
+ billed, so one browser serves a whole suite; and Browser Rendering allows only a
253
+ few concurrent sessions per account, which a parallel test run would otherwise
254
+ trip. An alarm closes the browser once idle — an *unclosed* session is billed too.
255
+
256
+ | Route | Does |
257
+ | --- | --- |
258
+ | `POST /state` | Store a Playwright storage state. Validated on the way in; the response summarises it and never echoes it |
259
+ | `GET /state` | A redacted summary. **There is no route that returns live cookies** |
260
+ | `DELETE /state` | Forget the session and close the browser |
261
+ | `POST /check` | Replay the session against a URL; reports `authenticated`, the landing URL, title, and optionally a screenshot |
262
+ | `POST /login` | Real Google sign-in, off unless `ALLOW_REAL_GOOGLE_LOGIN="true"`. Needs `loginUrl` **and** `expectUrl` |
263
+ | `POST /close` | Close the browser now rather than waiting for the alarm |
264
+
265
+ Every request must carry `x-test-auth-secret`. If `TEST_AUTH_SECRET` is not set
266
+ the Worker returns `503` to everything rather than serving an anonymous browser to
267
+ whoever finds the URL.
268
+
269
+ ```bash
270
+ curl -X POST "$WORKER/check?session=signed-in" \
271
+ -H "x-test-auth-secret: $TEST_AUTH_SECRET" \
272
+ -H 'content-type: application/json' \
273
+ -d '{"url":"https://app.example.test/dashboard","expectSelector":"[data-testid=user-menu]","rejectUrl":"/login"}'
274
+ ```
275
+
276
+ `rejectUrl` matters: a redirect to `/login` returns `200`, so without it a
277
+ signed-out check reads as a pass.
278
+
279
+ See [`examples/worker-check.ts`](examples/worker-check.ts) for the whole loop.
280
+
281
+ ### Carrying a session between Playwright and Puppeteer
282
+
283
+ `applyStorageState` / `extractStorageState` convert in both directions, and the
284
+ three differences between the formats are each a way to lose a session silently:
285
+
286
+ - **Session cookies.** Playwright writes `expires: -1`; CDP reads `-1` as an
287
+ expiry in 1969 and drops the cookie. The key must be *omitted*.
288
+ - **`sameSite`.** CDP wants exactly `Strict`, `Lax` or `None`. One lowercase
289
+ value rejects the whole batch.
290
+ - **`localStorage` needs a document.** There is no blind write — the page has to
291
+ be on the origin first, so restoring it costs one navigation per origin.
292
+
293
+ ```ts
294
+ import { applyStorageState } from "test-google-login";
295
+
296
+ await applyStorageState(page, state, { origins: ["https://app.example.test"] });
297
+ ```
298
+
299
+ Narrow `origins` to your own app. A state captured through a real Google sign-in
300
+ also holds `accounts.google.com` storage, and navigating a datacentre browser to
301
+ Google to restore it is slow and is exactly the traffic its risk checks look for.
302
+ A cookie-only state costs **no** navigation at all.
303
+
304
+ ## If you automate Google anyway
305
+
306
+ ```ts
307
+ import { signInWithGoogle } from "test-google-login";
308
+
309
+ setup("real Google OAuth smoke bootstrap", async ({ page, context }) => {
310
+ await signInWithGoogle({ page, context, loginPath: "/login" });
311
+ });
312
+ ```
313
+
314
+ It refuses to run unless `TEST_GOOGLE_LOGIN_ALLOW_REAL=1` **and** both
315
+ `GOOGLE_TEST_EMAIL` and `GOOGLE_TEST_PASSWORD` are set. Restrict that workflow to
316
+ a protected branch with environment-scoped secrets, trigger it by hand, and expect
317
+ failures that are not your app's fault — Google changes its UI, its labels and its
318
+ language, and a datacentre IP makes its risk checks *more* likely.
319
+
320
+ It waits for your own origin rather than clicking through whatever appears. If a
321
+ consent screen, a device check or MFA shows up, the wait times out, which is the
322
+ correct outcome: those are the controls protecting the account.
323
+
324
+ ## Refreshing an expired session
325
+
326
+ ```bash
327
+ npx test-google-login clear
328
+ npx playwright codegen --save-storage=playwright/.auth/google-test-user.json http://localhost:3000
329
+ ```
330
+
331
+ With the test-only endpoint, just re-run the setup project — it regenerates state
332
+ on its own.
333
+
334
+ Note that `storageState` covers cookies and `localStorage` (and IndexedDB when you
335
+ pass `indexedDB: true`), but **not `sessionStorage`**. If your app keeps anything
336
+ there, restore it yourself with an init script.
337
+
338
+ ## API
339
+
340
+ | Export | Does |
341
+ | --- | --- |
342
+ | `requireStoredSession(options)` | Assert a usable session exists; returns a safe summary, throws with the fix |
343
+ | `bootstrapAppSession(options)` | Mint a session through your test-only endpoint and persist it |
344
+ | `signInWithGoogle(options)` | Drive Google's real form. Off unless opted in |
345
+ | `loadStorageStateFor(options)` | Read the state for `browser.newContext({ storageState })` |
346
+ | `summarizeStorageState(state)` | Counts, domains, names, expiry — no values |
347
+ | `redactStorageState(state)` | The same state with every value replaced by its length |
348
+ | `findExpiringCookies(state, o)` | Cookies expiring inside a window — the warning before the failure |
349
+ | `assertStorageStateUsable(state, o)` | Throw unless still usable, with how long ago it lapsed |
350
+ | `readStorageState` / `writeStorageState` | Read; write 0600 |
351
+ | `hardenStorageStateFile(file)` | Chmod + validate a file Playwright wrote, preserving its extra keys |
352
+ | `applyStorageState(page, state, o)` | Restore a whole session into a Puppeteer page |
353
+ | `extractStorageState(page, o)` | Capture one back out, in Playwright's format |
354
+ | `to/fromPuppeteerCookies` | Cookie-format conversion, both directions |
355
+ | `ensureGitignored` / `writeAuthDirGitignore` | The two gitignore rules |
356
+ | `resolveAuthFile` / `codegenCommand` | Path resolution and the capture command |
357
+ | `constantTimeEqual` / `isAuthorized` | Secret comparison, usable in a Worker |
358
+ | `GoogleLoginBrowser` / `handleRequest` | The Durable Object and the Worker entry |
359
+
360
+ Full details in the agent skill:
361
+ [`skills/test-google-login`](../../skills/test-google-login/SKILL.md).
362
+
363
+ ## Development
364
+
365
+ ```bash
366
+ cd packages/test-google-login
367
+ bun run test # no browser is launched
368
+ bun run build # two entries: index (Node) and worker (Cloudflare)
369
+ bun run typecheck
370
+ ```
371
+
372
+ The suite drives fakes for the page, the browser and the DO's storage, so it runs
373
+ anywhere in under a second. The assertions that matter most are the negative ones:
374
+ that no value ever reaches a log, a response or a summary.
375
+
376
+ ## License
377
+
378
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ // Thin wrapper: the implementation is `runCli` in src/cli.ts, which returns an
3
+ // exit code instead of calling process.exit so it can be tested directly.
4
+ import { runCli } from "../dist/index.js";
5
+
6
+ process.exitCode = await runCli(process.argv.slice(2));
@@ -0,0 +1,91 @@
1
+ /** Playwright's documented directory for persisted auth state. */
2
+ export declare const DEFAULT_AUTH_DIR = "playwright/.auth";
3
+ /** The state file this package reads and writes unless told otherwise. */
4
+ export declare const DEFAULT_AUTH_FILE = "playwright/.auth/google-test-user.json";
5
+ /** Env var that overrides the state file path, for CI layouts that differ. */
6
+ export declare const STATE_PATH_ENV = "TEST_GOOGLE_LOGIN_STATE";
7
+ /**
8
+ * The lines `init` adds to `.gitignore`.
9
+ *
10
+ * `.env` is here too. Not scope creep: the same bootstrap that produces a state
11
+ * file is the one that puts `GOOGLE_TEST_PASSWORD` in a local `.env`, and a repo
12
+ * that ignores one but not the other is still one `git add -A` from a leak.
13
+ */
14
+ export declare const GITIGNORE_LINES: readonly ["playwright/.auth/", ".env", ".env.*", "!.env.example"];
15
+ export interface AuthFileOptions {
16
+ /** Project root. Defaults to `process.cwd()`. */
17
+ cwd?: string;
18
+ /** Explicit path, absolute or relative to `cwd`. */
19
+ file?: string;
20
+ /** Environment to read `TEST_GOOGLE_LOGIN_STATE` from. Defaults to `process.env`. */
21
+ env?: Record<string, string | undefined>;
22
+ }
23
+ /**
24
+ * The absolute path of the state file, resolving explicit argument → env var →
25
+ * default, in that order.
26
+ *
27
+ * @example
28
+ * resolveAuthFile() // <cwd>/playwright/.auth/google-test-user.json
29
+ * resolveAuthFile({ file: "ci/state.json" }) // <cwd>/ci/state.json
30
+ */
31
+ export declare function resolveAuthFile(options?: AuthFileOptions): string;
32
+ /**
33
+ * Create the directory a state file goes in, owner-only.
34
+ *
35
+ * `0o700` rather than the default `0o755`: on a shared CI box or a multi-user
36
+ * machine, a world-readable directory holding session cookies is the leak, and
37
+ * nobody notices because the file itself looks fine.
38
+ *
39
+ * @returns the absolute directory path
40
+ */
41
+ export declare function ensureAuthDir(file: string): string;
42
+ /**
43
+ * A second line of defence inside the auth directory itself: `playwright/.auth/.gitignore`
44
+ * containing `*`.
45
+ *
46
+ * The root `.gitignore` can be reverted, reformatted or replaced wholesale by a
47
+ * tool; a `.gitignore` living in the directory travels with it and keeps the
48
+ * state file ignored even if someone adds `!playwright/**` upstream of it.
49
+ */
50
+ export declare function writeAuthDirGitignore(file: string): string;
51
+ /**
52
+ * Does `.gitignore` already cover this pattern?
53
+ *
54
+ * Compares ignore *lines*, not paths: a real match check would mean
55
+ * reimplementing gitignore globbing, and being over-eager here means wrongly
56
+ * concluding a repo is safe. So `playwright/.auth/` counts as covered by
57
+ * `playwright/.auth`, `playwright/.auth/`, `/playwright/.auth/` or `playwright/`,
58
+ * and nothing cleverer. A pattern we fail to recognise costs a duplicate line,
59
+ * which is harmless.
60
+ */
61
+ export declare function isIgnored(gitignore: string, pattern: string): boolean;
62
+ export interface GitignoreResult {
63
+ /** Absolute path of the `.gitignore` written. */
64
+ file: string;
65
+ /** Patterns appended by this call. */
66
+ added: string[];
67
+ /** Patterns some existing line already covered. */
68
+ alreadyIgnored: string[];
69
+ }
70
+ /**
71
+ * Append whichever of {@link GITIGNORE_LINES} the repo does not already ignore.
72
+ *
73
+ * Idempotent, and never rewrites or reorders what is already there — a
74
+ * `.gitignore` is hand-maintained and a tool that reformats it gets turned off.
75
+ */
76
+ export declare function ensureGitignored(options?: {
77
+ cwd?: string;
78
+ patterns?: readonly string[];
79
+ }): GitignoreResult;
80
+ /**
81
+ * The `playwright codegen` command that captures a session by hand.
82
+ *
83
+ * Generated rather than pasted into prose, because it carries the state path —
84
+ * and a README telling you to save state to a path the suite does not read is a
85
+ * half-hour of confusion every time.
86
+ */
87
+ export declare function codegenCommand(options?: {
88
+ file?: string;
89
+ baseUrl?: string;
90
+ cwd?: string;
91
+ }): string;
package/dist/cli.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /** Parse `--flag value` and `--flag=value`, collecting the rest positionally. */
2
+ export declare function parseArgs(argv: string[]): {
3
+ command: string;
4
+ flags: Record<string, string | true>;
5
+ };
6
+ type Writer = (line: string) => void;
7
+ export interface CliIo {
8
+ out?: Writer;
9
+ err?: Writer;
10
+ cwd?: string;
11
+ }
12
+ /**
13
+ * Run the CLI.
14
+ *
15
+ * Returns an exit code rather than calling `process.exit`, and takes its writers
16
+ * as arguments, so the whole surface is testable without spawning anything.
17
+ */
18
+ export declare function runCli(argv: string[], io?: CliIo): Promise<number>;
19
+ export {};
@@ -0,0 +1,145 @@
1
+ import { StorageState, StorageStateSummary } from './types.js';
2
+ /** Env var that must be set before {@link signInWithGoogle} will run. */
3
+ export declare const ALLOW_REAL_LOGIN_ENV = "TEST_GOOGLE_LOGIN_ALLOW_REAL";
4
+ /** Header the test-only session endpoint authenticates with. */
5
+ export declare const BOOTSTRAP_HEADER = "x-e2e-auth-secret";
6
+ /** The subset of Playwright's `BrowserContext` used here. */
7
+ export interface ContextLike {
8
+ storageState(options?: {
9
+ path?: string;
10
+ indexedDB?: boolean;
11
+ }): Promise<unknown>;
12
+ }
13
+ /** The subset of Playwright's `APIRequestContext` used here. */
14
+ export interface RequestLike {
15
+ post(url: string, options?: {
16
+ headers?: Record<string, string>;
17
+ data?: unknown;
18
+ }): Promise<{
19
+ ok(): boolean;
20
+ status(): number;
21
+ text(): Promise<string>;
22
+ }>;
23
+ }
24
+ /** The subset of Playwright's `Locator` used here. */
25
+ export interface LocatorLike {
26
+ click(options?: Record<string, unknown>): Promise<void>;
27
+ fill(value: string, options?: Record<string, unknown>): Promise<void>;
28
+ }
29
+ /** The subset of Playwright's `Page` used here. */
30
+ export interface PlaywrightPageLike {
31
+ goto(url: string, options?: Record<string, unknown>): Promise<unknown>;
32
+ waitForURL(url: string | RegExp, options?: Record<string, unknown>): Promise<void>;
33
+ url(): string;
34
+ getByRole(role: string, options?: {
35
+ name?: string | RegExp;
36
+ }): LocatorLike;
37
+ getByLabel(text: string | RegExp, options?: Record<string, unknown>): LocatorLike;
38
+ }
39
+ export interface StoredSessionOptions {
40
+ /** State file path. Defaults to `playwright/.auth/google-test-user.json`. */
41
+ file?: string;
42
+ /** Project root for relative paths and for the hint in error messages. */
43
+ cwd?: string;
44
+ /** App URL used in the `playwright codegen` hint. */
45
+ baseUrl?: string;
46
+ /** Injectable clock, so expiry assertions are deterministic in tests. */
47
+ now?: number;
48
+ }
49
+ /**
50
+ * Assert that a usable persisted session exists, and describe it.
51
+ *
52
+ * This is the whole body of a Playwright setup project. It deliberately does not
53
+ * *create* the session: scripting Google's password form is the brittle part, so
54
+ * the first run tells you how to do it by hand and every run after that is a
55
+ * sub-millisecond file check.
56
+ *
57
+ * @returns a summary safe to print — no cookie value is in it
58
+ * @throws when the file is missing, malformed, or expired, in each case with the
59
+ * command that fixes it
60
+ *
61
+ * @example
62
+ * // tests/auth.setup.ts
63
+ * setup("Google test-user session exists", () => {
64
+ * const summary = requireStoredSession({ baseUrl: process.env.E2E_BASE_URL });
65
+ * console.log(`session ok — ${summary.cookieCount} cookies, expires in ${summary.expiresInSeconds}s`);
66
+ * });
67
+ */
68
+ export declare function requireStoredSession(options?: StoredSessionOptions): StorageStateSummary;
69
+ export interface BootstrapAppSessionOptions extends StoredSessionOptions {
70
+ /** Playwright's `request` fixture. */
71
+ request: RequestLike;
72
+ /** Playwright's `page` fixture — needed to put the app's origin in the context. */
73
+ page: PlaywrightPageLike;
74
+ /** Playwright's `context` fixture, which the state is read off. */
75
+ context: ContextLike;
76
+ /**
77
+ * The test-only endpoint that creates a session. Must 404 in production, and be
78
+ * covered by a test that proves it does.
79
+ */
80
+ endpoint?: string;
81
+ /** The shared secret the endpoint checks. Required; never defaulted. */
82
+ secret?: string;
83
+ /** Where to land afterwards, so the app's cookies are in the context. */
84
+ landingPath?: string;
85
+ /** Body posted to the endpoint — which user to create, what roles to give it. */
86
+ user?: Record<string, unknown>;
87
+ /** Capture IndexedDB too. Needed when your auth library keeps tokens there. */
88
+ indexedDB?: boolean;
89
+ }
90
+ /**
91
+ * Ask your own app for a session that looks exactly like a successful Google
92
+ * callback, then persist the browser state.
93
+ *
94
+ * The recommended path for CI. The only secret involved is yours, it is scoped to
95
+ * your own staging environment, and nothing in the run touches Google — so a PR
96
+ * check cannot fail because Google showed a consent screen.
97
+ *
98
+ * The endpoint on your side must create the *same* session format, claims, roles
99
+ * and cookies as the real callback. An endpoint that mints a subtly different
100
+ * session turns the whole suite into a test of a code path that does not ship.
101
+ *
102
+ * @throws when `secret` is absent — an unauthenticated session-minting endpoint
103
+ * is a far worse outcome than a failed test, so this never falls back
104
+ */
105
+ export declare function bootstrapAppSession(options: BootstrapAppSessionOptions): Promise<string>;
106
+ /** Is real Google sign-in opted into, and are the credentials present? */
107
+ export declare function realGoogleLoginEnabled(env?: Record<string, string | undefined>): boolean;
108
+ export interface GoogleSignInOptions extends StoredSessionOptions {
109
+ page: PlaywrightPageLike;
110
+ context: ContextLike;
111
+ /** Environment the opt-in flag and credentials are read from. */
112
+ env?: Record<string, string | undefined>;
113
+ /** Your app's login page. */
114
+ loginPath?: string;
115
+ /** Accessible name of your app's "sign in with Google" button. */
116
+ signInButton?: string | RegExp;
117
+ /** URL the flow must land on before the state is captured. */
118
+ expectUrl?: RegExp;
119
+ indexedDB?: boolean;
120
+ /** Google's own field and button labels, overridable because Google changes them. */
121
+ googleSelectors?: {
122
+ email?: RegExp;
123
+ password?: RegExp;
124
+ next?: RegExp;
125
+ };
126
+ }
127
+ /**
128
+ * Drive Google's real sign-in form and persist the result.
129
+ *
130
+ * Use this once, in a workflow a human triggers, to confirm your OAuth client is
131
+ * configured — not in a PR check. The selectors below are illustrative: Google
132
+ * changes its UI, its labels and its language, and none of that is something this
133
+ * package can keep up with.
134
+ *
135
+ * It will not attempt to get past MFA, CAPTCHA, a suspicious-sign-in prompt or
136
+ * device verification, and you should not add that: those are the controls
137
+ * protecting the account, and automating around them is how a test account
138
+ * becomes a compromised one. If you hit them, capture the session by hand with
139
+ * `playwright codegen` instead.
140
+ *
141
+ * @throws when {@link ALLOW_REAL_LOGIN_ENV} is not set or credentials are missing
142
+ */
143
+ export declare function signInWithGoogle(options: GoogleSignInOptions): Promise<string>;
144
+ /** Load a state file for passing to `browser.newContext({ storageState })`. */
145
+ export declare function loadStorageStateFor(options?: StoredSessionOptions): StorageState;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * test-google-login — persist a Google sign-in once, reuse it everywhere.
3
+ *
4
+ * The Node/Playwright half of the package. The Cloudflare Workers half is a
5
+ * separate entry point, `test-google-login/worker`, so that importing this one
6
+ * never drags a Worker runtime into your test process (and vice versa).
7
+ *
8
+ * Start with {@link requireStoredSession} in a Playwright setup project, or
9
+ * {@link bootstrapAppSession} if your app can mint its own test sessions — which
10
+ * is the one to use in CI.
11
+ *
12
+ * @see README.md for the security boundary around the state file. It is short,
13
+ * and it is the most important part of this package.
14
+ */
15
+ export type { ApplyStorageStateOptions, ApplyStorageStateResult, } from './puppeteer-state.js';
16
+ export type { PageLike, PuppeteerCookie, SameSite, StorageState, StorageStateCookie, StorageStateOrigin, StorageStateSummary, } from './types.js';
17
+ export type { BootstrapAppSessionOptions, ContextLike, GoogleSignInOptions, LocatorLike, PlaywrightPageLike, RequestLike, StoredSessionOptions, } from './google-login.js';
18
+ export type { CliIo } from './cli.js';
19
+ export type { GitignoreResult, AuthFileOptions } from './auth-file.js';
20
+ export { DEFAULT_AUTH_DIR, DEFAULT_AUTH_FILE, GITIGNORE_LINES, STATE_PATH_ENV, codegenCommand, ensureAuthDir, ensureGitignored, isIgnored, resolveAuthFile, writeAuthDirGitignore, } from './auth-file.js';
21
+ export { InvalidStorageStateError, MissingStorageStateError, SESSION_COOKIE_EXPIRES, assertStorageStateUsable, findExpiringCookies, hardenStorageStateFile, isGoogleDomain, normalizeStorageState, parseStorageState, readStorageState, redactStorageState, summarizeStorageState, writeStorageState, } from './storage-state.js';
22
+ export { ALLOW_REAL_LOGIN_ENV, BOOTSTRAP_HEADER, bootstrapAppSession, loadStorageStateFor, realGoogleLoginEnabled, requireStoredSession, signInWithGoogle, } from './google-login.js';
23
+ export { applyLocalStorage, applyStorageState, extractStorageState, fromPuppeteerCookies, storageStateOrigins, toPuppeteerCookies, } from './puppeteer-state.js';
24
+ export { AUTH_HEADER, constantTimeEqual, isAuthorized } from './secret.js';
25
+ export { parseArgs, runCli } from './cli.js';