208 Commits

Author SHA1 Message Date
metatroncubeswdev
17dfca406f Audit crawler: send a browser navigation header set (tier-1 anti-bot)
Some checks failed
CI / ci (push) Has been cancelled
CI / docker-build (push) Has been cancelled
Publish Docker image / docker (push) Has been cancelled
Upload sourcemaps / upload (push) Has been cancelled
The crawler identified as `OpenSEO-Audit/1.0` with almost no headers, which
naive bot filters and security plugins block outright.

- New crawl-request.ts: AUDIT_USER_AGENT (a current Chrome string),
  buildAuditHeaders() (Accept, Accept-Language, Sec-Fetch-*, Sec-Ch-Ua,
  Upgrade-Insecure-Requests), and fetchForAudit() — fetch + those headers +
  one retry on a transient 429/503.
- Wired into the page crawl (site-audit-workflow-helpers), robots.txt +
  sitemap discovery, and start-URL redirect probing.

Gets past the naive tier; still reported as "blocked" for JS/TLS challenges
(Cloudflare Managed Challenge, DataDome) — those need a real browser. Doc note
points operators at WAF IP/UA allowlisting for their own sites.

No env dependency (keeps the audit lib importable without a cloudflare:workers
mock). tsc / oxlint / knip clean; new crawl-request.test.ts (5); suite
otherwise unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-10 16:35:50 -04:00
metatroncubeswdev
ce75d45141 Phase 4: activity log — who did what in the workspace
Some checks failed
CI / ci (push) Has been cancelled
CI / docker-build (push) Has been cancelled
Publish Docker image / docker (push) Has been cancelled
Upload sourcemaps / upload (push) Has been cancelled
- activity_log table (sqlite + pg, structurally identical; schema-parity
  covers it). Plain-text columns, no FKs — an append-only trail that must
  outlive the projects/users it references, so target_label snapshots a
  human-readable name at write time.
- ActivityRepository: record() (fire-and-forget, never breaks the caller) +
  list() (org-scoped, actor/action filters, keyset pagination) + listActors().
- Recording wired into the mutations worth tracking: project
  create/archive/restore/domain, audit start, team user create/remove/
  password-reset, invitation sent.
- getActivityLog / getActivityActors server functions (owner/admin gated) +
  Settings → Activity tab (ActivityLogView: filter by user & action, load
  more).
- Migration: drizzle/0045_*, drizzle-pg/0023_*. The pipeline does not run
  migrations — see docs/SELF_HOSTING_TEAM_MODE.md step 5 for the one-time
  `drizzle-kit migrate` on the server. Writes fail silently until the table
  exists.

tsc / oxlint / knip clean. New ActivityRepository.test.ts (4) + schema-parity
picks up the new table; suite otherwise unchanged (pre-existing samSkills
CRLF failure only).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 16:00:09 -04:00
metatroncubeswdev
29bda614a4 Fix team-mode first-run: /setup no longer bounces to /sign-in
Some checks failed
CI / ci (push) Has been cancelled
CI / docker-build (push) Has been cancelled
Publish Docker image / docker (push) Has been cancelled
Upload sourcemaps / upload (push) Has been cancelled
- _auth.setup.tsx: the "owner already exists?" check ran in beforeLoad, which
  executes during SSR where fetchTeamSetupStatus's relative fetch to
  /api/team-setup fails — so it always concluded an owner existed and
  redirected to /sign-in, making the create-owner screen unreachable. Move the
  check into a client-side effect with a loading state.
- setup-status.ts: add BETTER_AUTH_URL to CHECK_ENV_VARS so /api/health stops
  falsely reporting "team mode requires BETTER_AUTH_URL" when it is set (the
  Docker preflight already saw it; only the runtime health check's env
  allowlist was missing it).

Verified locally end to end against a D1 build in AUTH_MODE=team: owner
bootstrap, self-disable + 409 on repeat, Better Auth sign-in issues a session
cookie, and get-session resolves the shared organization.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 14:19:28 -04:00
metatroncubeswdev
acc35c055c Phase 3: team-mode owner setup, user management, shared workspace
Some checks failed
CI / ci (push) Has been cancelled
CI / docker-build (push) Has been cancelled
Publish Docker image / docker (push) Has been cancelled
Upload sourcemaps / upload (push) Has been cancelled
Makes AUTH_MODE=team usable end to end.

- resolveTeamContext (middleware/ensure-user/team.ts): a session resolves to a
  membership in the single shared workspace. No per-user fallback org — a
  signed-in user with no membership is treated as signed out, so the owner can
  actually remove people.
- teamProvisioning.ts: one path that writes user + credential account + member
  together (hashPassword from better-auth/crypto). Shared by both entry points.
- /api/team-setup (raw route, outside auth middleware): GET reports whether an
  owner is needed; POST creates the first owner + the shared org, then
  self-disables once any user exists.
- /setup route + sign-in redirect: first run sends you to create the owner.
- teamUsers server functions (owner/admin-gated): list / create (with temp
  password) / reset password / remove. Removal drops membership + sessions,
  keeps the user row for historical attribution.
- Settings gains a "Users" tab in team mode (TeamUsers.tsx).
- docs/SELF_HOSTING_TEAM_MODE.md: activation runbook (.env, build, first owner).

No DB migration — all rows are existing better-auth tables. tsc / oxlint / knip
clean. New teamProvisioning.test.ts (4 cases) passes; suite otherwise unchanged
(pre-existing samSkills.test.ts CRLF failure only).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 08:13:01 -04:00
metatroncubeswdev
c47b032f1a Add team auth mode (backend, inert until AUTH_MODE=team)
Some checks failed
CI / ci (push) Has been cancelled
CI / docker-build (push) Has been cancelled
Publish Docker image / docker (push) Has been cancelled
Upload sourcemaps / upload (push) Has been cancelled
Introduces a fourth AUTH_MODE, `team`: Better Auth email/password with the
existing organization/member/role/invitation stack, but none of the hosted
SaaS coupling (no Autumn billing, Turnstile, Loops email, Google social
login, onboarding chat, PostHog, disposable-email block, dub referrals).

- auth-mode.ts: add `team`; add isTeamAuthMode / isSessionAuthMode /
  isSessionClientAuthMode ("is there a login session?" vs isHostedAuthMode's
  "is this the billed product?").
- auth.ts: createAuth() builds a valid instance for `team` — verification
  off, self-serve signup disabled, no captcha/Loops/social. hasTeamAuthConfig
  (BETTER_AUTH_URL + BETTER_AUTH_SECRET only) + hasSessionAuthConfig.
- ensure-user: resolve.ts routes `team` through resolveHostedContext;
  requireHostedSession + selfHostedOAuth callback accept any session mode.
- api/auth/$.ts: mount the Better Auth handler for `team` too.
- Client: route guards, sidebar account menu / sign-out, settings
  Organization tab, invitation accept, and error cards switch from
  isHostedClientAuthMode to isSessionClientAuthMode where they mean "has a
  session". Sign-in goes straight to the email form (no Google button);
  sign-up shows an invite-only notice.
- selfhost-preflight: validate `team` (requires BETTER_AUTH_URL +
  BETTER_AUTH_SECRET >= 32 chars).
- .env.example: document `team`.

Ships inert: AUTH_MODE stays local_noauth. tsc / oxlint / knip clean;
test suite unchanged (1164 pass, 1 pre-existing Windows-CRLF failure in
samSkills.test.ts).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 00:51:20 -04:00
Ben Senescu
ac9ee482d2
Revert "fix(audit): back off and retry on 429 instead of recording the page a…" (#564)
This reverts commit bb099ad65ae9ac50a6d900c5f61bd0a942661333.
2026-08-28 16:16:44 -04:00
Ben Senescu
ad2d28ea6f
fix(ga4): honor full date ranges instead of clamping to 90 days (#563) 2026-08-28 16:15:22 -04:00
Ben Senescu
bb099ad65a
fix(audit): back off and retry on 429 instead of recording the page as blocked (#562) 2026-08-28 15:27:23 -04:00
Ben Senescu
749b38118d
fix: show GA4 MCP rows in Claude text output (#553) 2026-08-27 18:02:13 -04:00
Ben Senescu
ea162a4391
feat(orgs): multi-user workspaces — roles, invitations, membership enforcement (#473) 2026-08-26 16:54:08 -04:00
Ben Senescu
4d7fb661f0
Switch chat agents to GPT-5.6 Luna (max reasoning) (#499) 2026-08-26 16:39:51 -04:00
Ben Senescu
61b32ec647
fix: extend OAuth state cookie lifetime to match the 10-minute state TTL (#544) 2026-08-26 14:37:46 -04:00
Ben Senescu
9b12e073a8
fix: replace the broken per-key API rate limit with a real 5000/min per-user /mcp limit (#545) 2026-08-26 12:09:31 -04:00
Ben Senescu
11a874ed3b
chore: log Lighthouse runtime errors at warn and include strategy (#539) 2026-08-26 11:51:59 -04:00
Ben Senescu
c7ff28c30a
chore: log rank-check live call failures with error code and keyword, warn on upstream flakes (#542) 2026-08-26 11:51:26 -04:00
Ben Senescu
824e914eb1
fix(mcp): refuse subscriptions/listen streams — MCP serving is stateless (EVE-95) (#548) 2026-08-26 11:48:20 -04:00
Ben Senescu
0abde80109
fix: add 10s timeout to Loops fetches (#543) 2026-08-26 11:41:22 -04:00
Ben Senescu
95551a425a
chore: log expected credit refusals in backlink snapshot refresh at info (#541) 2026-08-26 11:39:50 -04:00
Ben Senescu
022a7f1944
SERP analysis: depth 20 by default, load top 100 on demand; classify DataForSEO timeouts (#523) 2026-08-26 11:17:18 -04:00
Ben Senescu
a1c6eb6b11
fix: meta-only-response test used toSorted, banned by the ES2022 lib floor (#537) 2026-08-26 10:37:50 -04:00
Ben Senescu
0311e72a96
PostHog reports every server error as affecting as many users as it has events (#534) 2026-08-26 10:23:03 -04:00
Ben Senescu
8752269149
Stop cloning the raw Lighthouse report through Zod (#528) 2026-08-26 10:11:14 -04:00
Ben Senescu
94b730124b
DataForSEO's own server errors show as an unexpected error and retry 3x (#526) 2026-08-26 10:06:40 -04:00
Ben Senescu
67d68281e3
fix(mcp): accept expected partial outputs (#449) 2026-08-26 10:04:40 -04:00
Ben Senescu
b631715112
Blank page on older browsers: replace ES2023 array methods with Remeda (#535) 2026-08-26 10:02:03 -04:00
Ben Senescu
8db0dd3246
First site audit over MCP fails with a raw billing error (#525) 2026-08-26 09:59:35 -04:00
Ben Senescu
3fc1f4ec34
fix(dataforseo): accept empty live SERPs (#451) 2026-08-26 09:48:32 -04:00
Ben Senescu
b592dc5043
fix(mcp): classify project input validation (#455) 2026-08-26 09:39:09 -04:00
Ben Senescu
fcb35a8145
fix: move the site-audit engine to a dedicated open-seo-audit worker (stops audit OOMs) (#530) 2026-08-25 23:12:26 -04:00
Ben Senescu
215ead8152
Replace dataforseo-client SDK with a thin fetch client (#532) 2026-08-25 22:23:55 -04:00
Ben Senescu
ac7ebfe13a
Add Dub referral conversion tracking (leads + sales) (#531) 2026-08-25 21:49:08 -04:00
Ben Senescu
6e02186a2b Add ChatGPT plugin submission manifest (#519) 2026-08-23 18:18:17 -04:00
Ben Senescu
4ba2d6c175 Require exact hosted MCP origins (#515)
* Require exact hosted MCP origins

* Clarify exact MCP origin policy
2026-08-23 18:18:17 -04:00
Ben Senescu
e5e961bf48 Audit crawl: conservative OOM retries + window carry-over across chunks (#517)
* Audit crawl: retry chunks conservatively and stop re-learning the window per chunk

Production audits on heavy-page sites (500-700KB/page) died with
exceededMemory in the first crawl chunk, and the single step retry re-ran
the exact same profile and died again.

- claimChunk now reports isRetry (leftover leases from a dead attempt);
  a retried chunk crawls under RETRY_CRAWL_WINDOW (start 3, max 5,
  halved byte budget) instead of restarting at window 10.
- The adapted window carries across chunks via durable step results, so
  every ~200 pages no longer re-spikes to the initial window.
- First persist sub-batch shrinks to 5 pages so the byte bound sees the
  site's page weight before a full 25-page batch is in flight.
- In-flight HTML budget halved to 8 MiB (16 MiB never constrained the
  observed ~650KB pages: bound was 25, above the 20 max); growth now
  requires a full 25-page sample.
- Parse-time caps: 1,000 extracted links/images per page so mega-menu and
  crawler-trap pages can't bloat retained persist batches.

* Guard endWindow for instances replaying pre-deploy step results

* ci: un-export internal-only interfaces (knip)
2026-08-23 18:18:17 -04:00
Ben Senescu
6470b875ad Allow SurfMind Chrome extension MCP origin (#513) 2026-08-23 18:18:17 -04:00
Ben Senescu
47883b5d5a Fix GA4 measurement health for sparse API responses (#512) 2026-08-23 18:18:17 -04:00
Ben Senescu
43265048df
Tame audit polling: SAM's status tool waits server-side; Lighthouse becomes opt-in for agents (#510) 2026-08-19 14:22:33 -04:00
Ben Senescu
a6f96c516e
Remove GA4 launch gating; dashboard shows an Organic traffic card once connected (#505) 2026-08-19 13:19:55 -04:00
Ben Senescu
a5a953e478
Project memory + SAM skills: shared per-project AI context (spec 0010) (#493) 2026-08-19 09:46:07 -04:00
Ben Senescu
cfa5bc2cf5
Gate GA4 MCP tools per-user; fix false ga4_malformed_response on empty comparison periods (#497) 2026-08-18 15:56:38 -04:00
Ben Senescu
7738f72333
Local SEO MCP tools: business profile, reviews, updates, categories, rank grid (#489) 2026-08-17 23:21:34 -04:00
Ben Senescu
c8b3eb9b0a
Add exact URL / subfolder / domain / subdomain research scopes (EVE-56) (#487) 2026-08-14 21:52:32 -04:00
Ben Senescu
db45a0dd6d
Fix MCP saved keyword metrics (#483) 2026-08-13 20:16:20 -04:00
Ben Senescu
72eaa09c32
Hide Analytics section when GA4 OAuth app is pending (#475) 2026-08-12 19:57:29 -04:00
Ben Senescu
84e8d0be99
Serve legacy MCP JSON requests statelessly to stop per-request server retention (#478) 2026-08-11 20:15:48 -04:00
Ben Senescu
16eb599270
MCP API key support (hosted mode) (#438) 2026-08-11 19:20:44 -04:00
Ben Senescu
9edb18db60
feat(gdpr): user data erasure workflow (EVE-46) (#468) 2026-08-08 19:17:41 -04:00
Ben Senescu
13ada5b441
fix(sam): out-of-credits CoT leak, streaming scroll lock, rank-tracking language gap (#470)
* fix(sam): stream canned refusals without a provider call

Out-of-credits and session-gone refusals ran a real LLM turn with a
200-token cap; MiniMax M3 could spend the whole budget on reasoning
tokens, leaving the user a raw truncated chain-of-thought (which also
named the backing model) and no reply. Refusal turns now swap in a
static LanguageModelV3 that streams the refusal text through Think's
normal pipeline — rendered and persisted like any assistant message,
with no provider request at all.

Fixes every-app/open-seo#161

* fix(chat): stop pinning the transcript to the bottom while the user scrolls up

Both chat surfaces (SAM and onboarding) forced scrollTop to the bottom
on every streamed chunk, so scrolling up mid-reply was undone within
milliseconds. A shared stick-to-bottom hook now tracks pinned-ness from
real scroll events: scrolling away releases the pin, returning to the
bottom (or sending a message) re-arms it.

Fixes every-app/open-seo#160

* fix(rank-tracking): allow any SERP language for any country

The Add Domain modal restricted the language picker to the Labs
per-country subset and disabled it when only one option existed, so
e.g. tracking English searches in Czechia was impossible — even though
rank tracking runs against the SERP API, which serves every supported
language in every country. The picker now offers the full SERP language
list; create/update schemas validate codes against the master list so
unknown codes still fail before DataForSEO charges for them; and
keyword-metrics refreshes resolve a Labs-served language so an
unserved pair never reaches a charged Labs call.

Fixes every-app/open-seo#183

* fix(chat): surface silent turn failures in Workers logs

A provider stream dying mid-turn (chat:request:failed) and a DO restart
whose recovery gives up (chat:recovery:exhausted) leave the user a
replayed "Something went wrong" banner and a half-streamed message, but
never reach the onChatError hook — their only signal is the agents:chat
diagnostics channel, which was unsubscribed, so the chat agents' most
common failure modes produced zero log lines. A module-level
subscription now logs both for every chat DO.

SamChatAgent.onChatError also returns the error now: Think uses the
return value as the stored chat-terminal body that reconnecting clients
replay, and returning void stored the literal string "undefined".
2026-08-08 18:47:41 -04:00
Ben Senescu
17e7515e82
feat(self-host): share one workspace across Cloudflare Access users (#467) 2026-08-08 18:23:31 -04:00
Ben Senescu
2cedcda5db
fix(billing): activate paid plans immediately after checkout (EVE-44) (#469) 2026-08-08 13:06:45 -04:00