npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

automate-google-login-scraper

v0.1.26

Published

End-to-end test harness for Google sign-in: persist a Playwright storageState once, reuse it everywhere, and replay it from a Cloudflare Workers Browser Rendering Durable Object

Readme

🤖 Agent skillnpx skills@latest add https://github.com/OpenSourceAGI/dev-tools-starter-agent --skill test-google-login (what it covers)

Test Google Login

Sign in with Google once, by hand, and let every test after that start already signed in — locally, in CI, and from a Cloudflare Worker.

Two halves, one session file:

  • Playwright (test-google-login) — captures, validates, inspects and redacts a storageState, and guards a setup project so the first run tells a new contributor exactly what to do instead of timing out on a login form.
  • Cloudflare Workers (test-google-login/worker) — a Browser Rendering Durable Object that replays the same session against a deployed app, keeping one browser alive across a whole suite.

It has no runtime dependencies. Playwright and @cloudflare/puppeteer are optional peers: every function takes the page, context or request it needs as an argument, which is also why the whole package is unit-tested without ever launching a browser.

⚠ The security boundary — read this first

Putting GOOGLE_TEST_PASSWORD in .env protects nothing if you then write:

playwright/.auth/google-test-user.json

That file is a live credential. It can contain your app's session cookie, Google's cookies for accounts.google.com, OAuth tokens in localStorage or IndexedDB, and — depending on your auth library — something refresh-capable. Anyone holding it is signed in as the test account.

So:

  • Use a dedicated Google account for testing, with nothing sensitive on it.
  • Never commit it. test-google-login init adds the rules, twice.
  • Never upload it as a public CI artifact, and never paste it into a log, an issue or a chat. Use test-google-login redact when you need to show it.
  • Regenerate it when it stops working, rather than trying to repair it.
  • Prefer testing your app's own session. Keep real Google sign-in as a narrow, occasional smoke test.

This package is built around that last point. Everything that touches Google is opt-in and off by default; everything that touches the file is 0600.

Install

bun add -d test-google-login
npm install --save-dev test-google-login

Quick start

1. Set up the directory and the gitignore rules

npx test-google-login init
Created playwright/.auth/ (mode 0700)
Wrote   playwright/.auth/.gitignore — ignores everything in that directory
Added to .gitignore: playwright/.auth/, .env, .env.*, !.env.example

Now capture a session by hand — this avoids automating Google's password form,
which is what MFA, CAPTCHA and device checks all break:

  npx playwright codegen --save-storage=playwright/.auth/google-test-user.json http://localhost:3000

Two gitignores on purpose: the root one, and playwright/.auth/.gitignore containing *. The root file gets reverted, reformatted and replaced by tooling; the nested one travels with the directory.

2. Capture a session, by hand, once

Run the command init printed. In the window that opens: click Sign in with Google, use the dedicated test account, finish the redirect back to your app, confirm you are on an authenticated page, then close the window — Playwright writes the state file on exit.

This is manual on purpose. Google may ask for MFA, a CAPTCHA or a device check, and none of those should be automated around.

3. Reuse it

playwright.config.ts:

import { defineConfig, devices } from "@playwright/test";
import { DEFAULT_AUTH_FILE } from "test-google-login";

export default defineConfig({
  testDir: "./tests/e2e",
  use: { baseURL: process.env.E2E_BASE_URL ?? "http://localhost:3000" },
  projects: [
    { name: "auth-setup", testMatch: /.*\.setup\.ts/ },
    {
      name: "chromium",
      dependencies: ["auth-setup"],
      use: { ...devices["Desktop Chrome"], storageState: DEFAULT_AUTH_FILE },
    },
  ],
});

tests/auth.setup.ts:

import { test as setup } from "@playwright/test";
import { requireStoredSession } from "test-google-login";

setup("an authenticated session is available", () => {
  const summary = requireStoredSession({ baseUrl: process.env.E2E_BASE_URL });
  console.log(`session ok — ${summary.cookieCount} cookies, ${summary.domains.join(", ")}`);
});

Your tests now start signed in:

test("an authenticated user can open the dashboard", async ({ page }) => {
  await page.goto("/dashboard");
  await expect(page.getByRole("heading", { name: /dashboard/i })).toBeVisible();
});

requireStoredSession is the part that earns its keep. A missing file throws the codegen command; an expired one throws how long ago it lapsed:

The persisted session is no longer usable — it expired 380 minutes ago.

Re-capture it:
  npx playwright codegen --save-storage=playwright/.auth/google-test-user.json http://localhost:3000

Without that, an expired session presents as a selector timeout on an unauthenticated page, which reads like a broken test rather than a stale cookie.

For CI: let your app mint the session

Do not make a Google password a repository secret every fork's pull request can reach. Instead, add a test-only endpoint to your own staging environment that creates the same session a successful Google callback creates, and hold one secret of yours.

import { bootstrapAppSession } from "test-google-login";

setup("create authenticated test session", async ({ page, context, request }) => {
  await bootstrapAppSession({
    request,
    page,
    context,
    secret: process.env.E2E_TEST_AUTH_SECRET,   // required; never defaulted
    endpoint: "/api/test-auth/google-user",
    landingPath: "/dashboard",
  });
});

State is regenerated each run, no Google traffic is involved, and a PR check can never fail because Google showed a consent screen. See examples/test-auth-endpoint.ts for the endpoint — it must 404 in production, require a strong secret compared in constant time, and create a session identical to the real callback's.

Which strategy where

| Strategy | Use for | Secrets held | | --- | --- | --- | | bootstrapAppSession — your app mints it | every PR check | E2E_TEST_AUTH_SECRET | | requireStoredSession — captured by hand | local development | none | | Encrypted stored state | private, controlled CI | the encryption key; delete state after the run | | signInWithGoogle — real Google | occasional manual smoke test | a dedicated account's credentials, environment-scoped | | Mock the OAuth callback | unit and integration tests | none |

Inspecting a session without opening it

npx test-google-login check
State file   playwright/.auth/google-test-user.json
Cookies      3 (1 session-only)
Domains      .google.com, app.example.test
Origins      1
Google cookies present: yes
Expiry       earliest in 718 minutes

Exit codes are meant for CI: 0 healthy, 1 missing or expired, 2 present but malformed. --json for machine-readable output, --within 30 to warn about cookies expiring inside a window.

npx test-google-login redact   # safe to paste into an issue
npx test-google-login clear    # delete the session

redact keeps the shape — names, domains, flags, expiries — and replaces every value with «redacted 21 chars». Nothing in this package ever prints a value: summarizeStorageState returns names and domains only, and the Worker's GET /state returns a redacted copy.

The Cloudflare Workers half

import { GoogleLoginBrowser, handleRequest } from "test-google-login/worker";

export { GoogleLoginBrowser };
export default { fetch: handleRequest };
wrangler secret put TEST_AUTH_SECRET
wrangler deploy

A Durable Object per named session, holding one Browser Rendering browser. Why a DO rather than puppeteer.launch() in the Worker: a launch costs seconds and is billed, so one browser serves a whole suite; and Browser Rendering allows only a few concurrent sessions per account, which a parallel test run would otherwise trip. An alarm closes the browser once idle — an unclosed session is billed too.

| Route | Does | | --- | --- | | POST /state | Store a Playwright storage state. Validated on the way in; the response summarises it and never echoes it | | GET /state | A redacted summary. There is no route that returns live cookies | | DELETE /state | Forget the session and close the browser | | POST /check | Replay the session against a URL; reports authenticated, the landing URL, title, and optionally a screenshot | | POST /login | Real Google sign-in, off unless ALLOW_REAL_GOOGLE_LOGIN="true". Needs loginUrl and expectUrl | | POST /close | Close the browser now rather than waiting for the alarm |

Every request must carry x-test-auth-secret. If TEST_AUTH_SECRET is not set the Worker returns 503 to everything rather than serving an anonymous browser to whoever finds the URL.

curl -X POST "$WORKER/check?session=signed-in" \
  -H "x-test-auth-secret: $TEST_AUTH_SECRET" \
  -H 'content-type: application/json' \
  -d '{"url":"https://app.example.test/dashboard","expectSelector":"[data-testid=user-menu]","rejectUrl":"/login"}'

rejectUrl matters: a redirect to /login returns 200, so without it a signed-out check reads as a pass.

See examples/worker-check.ts for the whole loop.

Carrying a session between Playwright and Puppeteer

applyStorageState / extractStorageState convert in both directions, and the three differences between the formats are each a way to lose a session silently:

  • Session cookies. Playwright writes expires: -1; CDP reads -1 as an expiry in 1969 and drops the cookie. The key must be omitted.
  • sameSite. CDP wants exactly Strict, Lax or None. One lowercase value rejects the whole batch.
  • localStorage needs a document. There is no blind write — the page has to be on the origin first, so restoring it costs one navigation per origin.
import { applyStorageState } from "test-google-login";

await applyStorageState(page, state, { origins: ["https://app.example.test"] });

Narrow origins to your own app. A state captured through a real Google sign-in also holds accounts.google.com storage, and navigating a datacentre browser to Google to restore it is slow and is exactly the traffic its risk checks look for. A cookie-only state costs no navigation at all.

If you automate Google anyway

import { signInWithGoogle } from "test-google-login";

setup("real Google OAuth smoke bootstrap", async ({ page, context }) => {
  await signInWithGoogle({ page, context, loginPath: "/login" });
});

It refuses to run unless TEST_GOOGLE_LOGIN_ALLOW_REAL=1 and both GOOGLE_TEST_EMAIL and GOOGLE_TEST_PASSWORD are set. Restrict that workflow to a protected branch with environment-scoped secrets, trigger it by hand, and expect failures that are not your app's fault — Google changes its UI, its labels and its language, and a datacentre IP makes its risk checks more likely.

It waits for your own origin rather than clicking through whatever appears. If a consent screen, a device check or MFA shows up, the wait times out, which is the correct outcome: those are the controls protecting the account.

Refreshing an expired session

npx test-google-login clear
npx playwright codegen --save-storage=playwright/.auth/google-test-user.json http://localhost:3000

With the test-only endpoint, just re-run the setup project — it regenerates state on its own.

Note that storageState covers cookies and localStorage (and IndexedDB when you pass indexedDB: true), but not sessionStorage. If your app keeps anything there, restore it yourself with an init script.

API

| Export | Does | | --- | --- | | requireStoredSession(options) | Assert a usable session exists; returns a safe summary, throws with the fix | | bootstrapAppSession(options) | Mint a session through your test-only endpoint and persist it | | signInWithGoogle(options) | Drive Google's real form. Off unless opted in | | loadStorageStateFor(options) | Read the state for browser.newContext({ storageState }) | | summarizeStorageState(state) | Counts, domains, names, expiry — no values | | redactStorageState(state) | The same state with every value replaced by its length | | findExpiringCookies(state, o) | Cookies expiring inside a window — the warning before the failure | | assertStorageStateUsable(state, o) | Throw unless still usable, with how long ago it lapsed | | readStorageState / writeStorageState | Read; write 0600 | | hardenStorageStateFile(file) | Chmod + validate a file Playwright wrote, preserving its extra keys | | applyStorageState(page, state, o) | Restore a whole session into a Puppeteer page | | extractStorageState(page, o) | Capture one back out, in Playwright's format | | to/fromPuppeteerCookies | Cookie-format conversion, both directions | | ensureGitignored / writeAuthDirGitignore | The two gitignore rules | | resolveAuthFile / codegenCommand | Path resolution and the capture command | | constantTimeEqual / isAuthorized | Secret comparison, usable in a Worker | | GoogleLoginBrowser / handleRequest | The Durable Object and the Worker entry |

Full details in the agent skill: skills/test-google-login.

Development

cd packages/test-google-login
bun run test        # no browser is launched
bun run build       # two entries: index (Node) and worker (Cloudflare)
bun run typecheck

The suite drives fakes for the page, the browser and the DO's storage, so it runs anywhere in under a second. The assertions that matter most are the negative ones: that no value ever reaches a log, a response or a summary.

License

MIT