diff --git a/docs/specs/hosted.md b/docs/specs/hosted.md index b7fa86780..414698c58 100644 --- a/docs/specs/hosted.md +++ b/docs/specs/hosted.md @@ -245,22 +245,24 @@ Source of truth: `relayApiRoutes` in `hosted/server/relay-api.ts`; `relayAccount ## Metrics -Hosted counts what its Workers serve in aggregate daily rows, server-side; no page or desktop sends any. +Hosted's Workers count what they serve in aggregate daily rows; no page or desktop sends any. | Event | Labels | Counted when | |---|---|---| +| `account.created` | none | a user row commits | +| `login` | `email`, `providerIds` | a sign-in's session commits, by method; never a link | | `enroll.approved` | `remote`, `voice` | an approval succeeds, under its purpose | | `burrow.enrolled` | none | a remote poll enrolls a Burrow | | `voice.speak` | `ok`, `capped`, `error` | speak answers 200, 429, or 502 | | `voice.fallback-cap` | none | a speak spends its owner's last call of the UTC day | -| `push.sent` | none | a send delivers, once per delivery | +| `push.sent` | none | each delivery a send makes | | `pocket.signin` | none | a passkey sign-in mints a session | -- **Never count a label off its event's list** (`METRIC_LABELS`): the write throws instead. What a row may hold, and who reads it: `docs/specs/security-hosted.md` -> "Account boundary". -- **Never fail or delay the request a metric counts**: the write runs after the response, through `waitUntil`, and its failure is only logged. -- Rows are kept forever, since none describes anyone. The account Worker's `/admin/metrics` shows the last 30 UTC days and all-time totals, "today" by the database's clock, which dates every row. +- **Never count a label off its event's list** (`METRIC_LABELS`): the write throws. Row contents and readers: `docs/specs/security-hosted.md` -> "Account boundary". +- **Never fail or delay the request a metric counts**: the write is unawaited, kept alive by `waitUntil`, and only logs failure. +- Rows are kept forever (rationale). The database's UTC clock dates every row and the account Worker's `/admin/metrics` "today". -Source of truth: `hosted/server/metric-labels.ts`; `recordMetric` in `hosted/server/metrics.ts`; `hosted/server/dormouse-migrations/013_metrics.sql`; `AdminMetrics` in `hosted/src/AdminMetrics.tsx`. +Source of truth: `hosted/server/metric-labels.ts`; `recordMetric` and `authMetricHooks` in `hosted/server/metrics.ts`; `hosted/server/dormouse-migrations/013_metrics.sql`; `AdminMetrics` in `hosted/src/AdminMetrics.tsx`. ## Development and release diff --git a/docs/specs/hosted.rationale.md b/docs/specs/hosted.rationale.md index d7c83946e..3c4d14746 100644 --- a/docs/specs/hosted.rationale.md +++ b/docs/specs/hosted.rationale.md @@ -66,3 +66,7 @@ A `RelayRoom` that opened a Postgres connection through Hyperdrive could not be - A passkey outlives its Burrow (2026-10-20): Pocket signs in to every Burrow of the account with one passkey, retiring a later computer's setup code rather than registering again, and each Burrow's pairings pin that passkey, so removing the computer whose code added it would sign the phone out of the others and force it to pair with each again. The account page removes a passkey on its own instead, naming the computer that added it while that computer is enrolled. - Confirmation digits (2026-10-20): the user code alone was passive, so a person who clicked a stranger's link and Approve without comparing handed that stranger an enrollment or a voice token. The digits make approving active, as pairing's are: only someone looking at the computer that asked can type them, and a link never carries them. They are derived rather than stored so begin still writes nothing, and checked at the poll because the account Worker never holds `RELAY_ENROLL_SECRET` (`docs/specs/security-hosted.md` -> "Origin boundary"); the account page therefore cannot say a typo was wrong, and the desktop reports it. One attempt at 1/100 per code, the approval limit bounding attempts per account. Desktops through v1.3.0 show no digits, so they cannot finish a Hosted approval: a path without digits would be a downgrade anyone could choose. - Guessing: approving a well-formed code no Burrow holds enrolls nothing; to steal a pending enrollment the account would have to be the one approving its code, which is the flow itself. + +## Metrics + +Rows are kept forever because none describes anyone: a row holds a day, an event, a fixed label, and a count, never an identifier (`docs/specs/security-hosted.md` -> "Account boundary"). diff --git a/hosted/package.json b/hosted/package.json index 5cde29a9d..790f8a386 100644 --- a/hosted/package.json +++ b/hosted/package.json @@ -22,8 +22,8 @@ "test:dom": "vitest run src" }, "dependencies": { - "@pgstencil/auth": "^0.4.0", - "pgstencil": "^0.4.0", + "@pgstencil/auth": "^0.4.1", + "pgstencil": "^0.4.1", "kysely": "^0.29.5", "hono": "^4.13.8", "@hono/node-server": "^2.0.10", diff --git a/hosted/server/account-auth.ts b/hosted/server/account-auth.ts index 066752fe9..b04ecfd67 100644 --- a/hosted/server/account-auth.ts +++ b/hosted/server/account-auth.ts @@ -1,10 +1,12 @@ import { createBetterAuthWorker } from "@pgstencil/auth/better-auth-workers"; import { postmarkEmail } from "@pgstencil/auth/postmark"; import type { AccountEnv } from "./bindings"; +import { authMetricHooks } from "./metrics"; import { authPolicy } from "./policy"; /** The account Worker's Better Auth handler, mailing through Postmark. */ export const auth = createBetterAuthWorker({ ...authPolicy, email: (env) => postmarkEmail(env.POSTMARK_SERVER_TOKEN, env.EMAIL_FROM), + hooks: (env) => authMetricHooks(env.HYPERDRIVE.connectionString), }); diff --git a/hosted/server/dev.ts b/hosted/server/dev.ts index 3c5650733..a64b8f4a6 100644 --- a/hosted/server/dev.ts +++ b/hosted/server/dev.ts @@ -9,6 +9,7 @@ import { createServer as createViteServer, type ViteDevServer } from "vite"; import { createAuthApp } from "@pgstencil/auth/better-auth"; import { developmentDatabase } from "pgstencil/database"; import { EmailDev, SystemTime } from "pgstencil"; +import { authMetricHooks } from "./metrics"; import { authPolicy } from "./policy"; import { migrations } from "./migrations"; import { allowedDevRequest } from "./dev-host-guard"; @@ -58,6 +59,7 @@ const auth = createAuthApp({ origin, secret: "dormouse-hosted-local-development-only", email, + hooks: authMetricHooks(databaseUrl), }); auth.app.get("/api/dev/emails", (c) => c.json(email.all().map(({ to, text }) => ({ to, text }))), diff --git a/hosted/server/metric-labels.ts b/hosted/server/metric-labels.ts index 063006dd7..8274ff6b8 100644 --- a/hosted/server/metric-labels.ts +++ b/hosted/server/metric-labels.ts @@ -1,11 +1,14 @@ // Rules: docs/specs/hosted.md -> "Metrics". No I/O: the admin page's bundle // imports this as well as the Workers. +import { providerIds } from "./providers.js"; /** * Every event Hosted counts, and the only labels each may carry; `[""]` for * one counted without a label. */ export const METRIC_LABELS = { + "account.created": [""], + login: ["email", ...providerIds], "enroll.approved": ["remote", "voice"], "burrow.enrolled": [""], "voice.speak": ["ok", "capped", "error"], diff --git a/hosted/server/metrics.ts b/hosted/server/metrics.ts index 01b26848c..ffcf3432e 100644 --- a/hosted/server/metrics.ts +++ b/hosted/server/metrics.ts @@ -1,4 +1,5 @@ // Rules: docs/specs/hosted.md -> "Metrics". The labels: ./metric-labels.ts. +import type { AuthHooks } from "@pgstencil/auth/better-auth"; import type { Context } from "hono"; import { queryDatabase } from "pgstencil/postgres"; import { @@ -12,7 +13,7 @@ import { /** * Adds `n` to today's (UTC) count of `event` under `label`. Rejects on a * label off the event's list or a database failure; callers go through - * {@link recordMetric}. + * {@link recordMetric} or {@link authMetricHooks}. */ export async function countMetric(databaseUrl: string, event: MetricEvent, label = "", n = 1) { await queryDatabase( @@ -24,17 +25,21 @@ export async function countMetric(databaseUrl: string, event: MetricEvent, label ); } +/** `write`, settled: it never rejects, and its failure is only logged. */ +const loggedFailure = (what: string, write: Promise) => + write.then( + () => {}, + (error: unknown) => + console.error(`${what} not recorded: ${error instanceof Error ? error.message : "unknown error"}`), + ); + /** * Lets `write` finish after the response, through the request's `waitUntil`, * and only logs its failure: it never fails or delays the request it rides * on. The Node dev loop has no ExecutionContext; there it runs unawaited. */ export function bestEffort(c: Context, what: string, write: Promise) { - const settled = write.then( - () => {}, - (error: unknown) => - console.error(`${what} not recorded: ${error instanceof Error ? error.message : "unknown error"}`), - ); + const settled = loggedFailure(what, write); try { c.executionCtx.waitUntil(settled); } catch { @@ -42,6 +47,16 @@ export function bestEffort(c: Context, what: string, write: Promise) { } } +/** + * The account Worker's sign-up and login counts. pgstencil runs each hook + * after its write commits, never awaits it, and never fires one for linking a + * provider; the Workers adapter keeps it alive with `waitUntil`. + */ +export const authMetricHooks = (databaseUrl: string): AuthHooks => ({ + userCreated: () => loggedFailure("Metric account.created", countMetric(databaseUrl, "account.created")), + login: ({ method }) => loggedFailure("Metric login", countMetric(databaseUrl, "login", method)), +}); + /** {@link countMetric}, best effort. */ export const recordMetric = (c: Context, databaseUrl: string, event: MetricEvent, label = "", n = 1) => bestEffort(c, `Metric ${event}`, countMetric(databaseUrl, event, label, n)); diff --git a/hosted/server/preview-worker.ts b/hosted/server/preview-worker.ts index ee9716657..aff79d92f 100644 --- a/hosted/server/preview-worker.ts +++ b/hosted/server/preview-worker.ts @@ -1,6 +1,7 @@ import { createBetterAuthWorker } from "@pgstencil/auth/better-auth-workers"; import { accountApp } from "./account-app"; import { accountPreviewBindings, type AccountEnv } from "./bindings"; +import { authMetricHooks } from "./metrics"; import { authPolicy } from "./policy"; import { postgresInbox, inboxPage, messagePage } from "./preview-inbox"; @@ -8,6 +9,7 @@ import { postgresInbox, inboxPage, messagePage } from "./preview-inbox"; const auth = createBetterAuthWorker({ ...authPolicy, email: (env) => postgresInbox(env.HYPERDRIVE.connectionString), + hooks: (env) => authMetricHooks(env.HYPERDRIVE.connectionString), }); export default accountApp( auth.fetch, diff --git a/hosted/server/tests/metrics.test.ts b/hosted/server/tests/metrics.test.ts index 2101b6e91..70eaef81d 100644 --- a/hosted/server/tests/metrics.test.ts +++ b/hosted/server/tests/metrics.test.ts @@ -4,7 +4,7 @@ import { createTestContext } from "pgstencil/testing"; import { queryDatabase } from "pgstencil/postgres"; import { migrations } from "../migrations"; import { METRIC_LABELS, metricLabel, type MetricEvent } from "../metric-labels"; -import { bestEffort, countMetric, readMetrics } from "../metrics"; +import { authMetricHooks, bestEffort, countMetric, readMetrics } from "../metrics"; import { workerDatabases } from "./worker-roles"; // docs/specs/hosted.md -> "Metrics": aggregate daily counts under fixed @@ -143,3 +143,15 @@ test("a metric's write never fails or delays the response it rides on, and its f error.mockRestore(); } }); + +test("the auth hooks never reject: a login by a method off the list is only logged", async () => { + const error = vi.spyOn(console, "error").mockImplementation(() => {}); + try { + // pgstencil's providers include one Hosted never enables. + await expect(authMetricHooks(context.database.url).login!({ method: "facebook" })).resolves.toBeUndefined(); + expect(error).toHaveBeenCalledWith('Metric login not recorded: login takes no label "facebook"'); + expect(await today("login", "facebook")).toBe(0); + } finally { + error.mockRestore(); + } +}); diff --git a/hosted/server/tests/workers.test.ts b/hosted/server/tests/workers.test.ts index 646e55bbe..a1c69a0d8 100644 --- a/hosted/server/tests/workers.test.ts +++ b/hosted/server/tests/workers.test.ts @@ -525,8 +525,17 @@ test("independent logins, current-browser logout, 24-hour expiry and private coo expect(await second.session()).toBeNull(); }); +/** Today's (UTC) count of `event` under `label`, once its best-effort write landed. */ +const counted = (database: string, event: string, label = "") => + queryDatabase<{ count: string }>( + database, + `SELECT count FROM dormouse_metrics_daily + WHERE day = (now() AT TIME ZONE 'UTC')::date AND event = $1 AND label = $2`, + [event, label], + ).then(([row]) => Number(row?.count ?? 0)); + test.for(providerIds)( - "%s: callback, replay and explicit linking", + "%s: callback, replay and explicit linking; counted as logins by method, never a link", async (id, { onTestFinished }) => { const f = await fixture(); onTestFinished(f.close); @@ -556,6 +565,14 @@ test.for(providerIds)( expect( (await owner.request(linked.path)).headers.get("location"), ).toContain("/login?error="); + // One account; its email login and its provider login, but not the link. + await vi.waitFor(async () => + expect([ + await counted(f.database.url, "account.created"), + await counted(f.database.url, "login", "email"), + await counted(f.database.url, "login", id), + ]).toEqual([1, 1, 1]), + ); }, ); @@ -1012,15 +1029,6 @@ test("Natural Voice: only the verified admin speaks; the account sees on or off expect((await admin.speak(lapsed, hi)).status).toBe(401); }); -/** Today's (UTC) count of `event` under `label`, once its best-effort write landed. */ -const counted = (database: string, event: string, label = "") => - queryDatabase<{ count: string }>( - database, - `SELECT count FROM dormouse_metrics_daily - WHERE day = (now() AT TIME ZONE 'UTC')::date AND event = $1 AND label = $2`, - [event, label], - ).then(([row]) => Number(row?.count ?? 0)); - test("metrics: speech by outcome and the call that spends the day, approvals by purpose, an enrollment; a failing write changes no answer", async ({ onTestFinished, }) => { @@ -1094,6 +1102,11 @@ test("the admin page: its read answers only the verified admin, from this origin await other.email("other@example.test"); expect((await read(other)).status).toBe(404); await admin.email(ADMIN_EMAIL); + // Only the row inserted below, once both sign-ins' counts have landed. + await vi.waitFor(async () => + expect([await counted(f.database.url, "account.created"), await counted(f.database.url, "login", "email")]).toEqual([2, 2]), + ); + await queryDatabase(f.database.url, "DELETE FROM dormouse_metrics_daily"); await queryDatabase( f.database.url, `INSERT INTO dormouse_metrics_daily (day, event, label, count) diff --git a/hosted/src/AdminMetrics.tsx b/hosted/src/AdminMetrics.tsx index ecd2eae22..0418a3342 100644 --- a/hosted/src/AdminMetrics.tsx +++ b/hosted/src/AdminMetrics.tsx @@ -19,6 +19,8 @@ import { HostedShell } from "./HostedShell"; export const isAdminMetricsPath = (path: string) => path === "/admin/metrics" || path === "/admin/metrics/"; const EVENT_TITLES = { + "account.created": "Accounts created", + login: "Logins", "enroll.approved": "Approvals", "burrow.enrolled": "Computers enrolled", "voice.speak": "Natural Voice requests", diff --git a/hosted/src/admin-metrics.test.ts b/hosted/src/admin-metrics.test.ts index 810453de5..899431f92 100644 --- a/hosted/src/admin-metrics.test.ts +++ b/hosted/src/admin-metrics.test.ts @@ -41,6 +41,8 @@ describe("summarize", () => { it("lists every event, one never counted at zero, and no label rows for an unlabeled one", () => { const summaries = summarize(body); expect(summaries.map((summary) => summary.event)).toEqual([ + "account.created", + "login", "enroll.approved", "burrow.enrolled", "voice.speak", diff --git a/hosted/src/stories/AdminMetrics.stories.tsx b/hosted/src/stories/AdminMetrics.stories.tsx index 55fa89b9c..1e758d05a 100644 --- a/hosted/src/stories/AdminMetrics.stories.tsx +++ b/hosted/src/stories/AdminMetrics.stories.tsx @@ -16,6 +16,11 @@ const recent: MetricRow[] = Array.from({ length: 30 }, (_, i) => 29 - i).flatMap const grown = 30 - back; if (back === 12) return []; return [ + { day: day(back), event: "account.created", label: "", count: 1 + (grown % 5) }, + { day: day(back), event: "login", label: "email", count: grown * 2 }, + { day: day(back), event: "login", label: "github", count: grown * 3 }, + ...(back % 2 === 0 ? [{ day: day(back), event: "login" as const, label: "google", count: grown }] : []), + ...(back === 3 ? [{ day: day(back), event: "login" as const, label: "apple", count: 1 }] : []), { day: day(back), event: "enroll.approved", label: "remote", count: 1 + (grown % 4) }, ...(back % 3 === 0 ? [{ day: day(back), event: "enroll.approved" as const, label: "voice", count: 1 }] : []), { day: day(back), event: "burrow.enrolled", label: "", count: 1 + (grown % 3) }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4c4a93f3c..bc551b9be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -339,8 +339,8 @@ importers: specifier: ^2.0.10 version: 2.1.5(hono@4.13.14) '@pgstencil/auth': - specifier: ^0.4.0 - version: 0.4.0(hono@4.13.14)(kysely@0.29.7)(pg@8.23.1)(pgstencil@0.4.0(cheerio@1.2.0)(kysely@0.29.7)(node-pg-migrate@9.0.0(pg@8.23.1))(testcontainers@12.2.1(supports-color@10.2.2))(turndown@7.2.4))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5) + specifier: ^0.4.1 + version: 0.4.1(hono@4.13.14)(kysely@0.29.7)(pg@8.23.1)(pgstencil@0.4.1(cheerio@1.2.0)(kysely@0.29.7)(node-pg-migrate@9.0.0(pg@8.23.1))(testcontainers@12.2.1(supports-color@10.2.2))(turndown@7.2.4))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5) '@phosphor-icons/react': specifier: ^2.1.10 version: 2.1.10(react-dom@19.3.1(react@19.3.1))(react@19.3.1) @@ -354,8 +354,8 @@ importers: specifier: ^0.29.5 version: 0.29.7 pgstencil: - specifier: ^0.4.0 - version: 0.4.0(cheerio@1.2.0)(kysely@0.29.7)(node-pg-migrate@9.0.0(pg@8.23.1))(testcontainers@12.2.1(supports-color@10.2.2))(turndown@7.2.4) + specifier: ^0.4.1 + version: 0.4.1(cheerio@1.2.0)(kysely@0.29.7)(node-pg-migrate@9.0.0(pg@8.23.1))(testcontainers@12.2.1(supports-color@10.2.2))(turndown@7.2.4) react: specifier: ^19.2.6 version: 19.3.1 @@ -1034,13 +1034,13 @@ packages: '@balena/dockerignore@1.0.2': resolution: {integrity: sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==} - '@better-auth/core@1.7.10': - resolution: {integrity: sha512-c5vxtnk7UxWe/iF/X5sKSgmPcN5n6biuwJBs2dMkceMwvMWBHLosue6h/vmvKns9SYKbkBNWsurFe7yIbv13dQ==} + '@better-auth/core@1.7.12': + resolution: {integrity: sha512-SUStBGHJ13KnSO6+7U/1DSlfuOtln4jtOrFZqzQtxZVHZIrAoMjwHNGitlF+s7hvKbuC3Z14xjnqfLLqIqGfxw==} peerDependencies: '@better-auth/utils': ^0.4.2 || ^0.5.0 '@better-fetch/fetch': 1.3.2 '@opentelemetry/api': ^1.9.0 - better-call: 1.4.2 + better-call: 1.4.4 jose: ^6.1.0 kysely: ^0.28.5 || ^0.29.0 nanostores: ^1.0.1 @@ -1048,46 +1048,46 @@ packages: '@opentelemetry/api': optional: true - '@better-auth/drizzle-adapter@1.7.10': - resolution: {integrity: sha512-Uc/Eh9cnvorIWA6J97Hv8zjR8sCVUP6G7M3xHJGTD9qJXC69zoY4ULmNGA73AmGJB1q+VEYs2cRMtJ6fR6HP6g==} + '@better-auth/drizzle-adapter@1.7.12': + resolution: {integrity: sha512-Qzoyuf05aK01Pc0k3EXv8Xj3XCT2m2dCWc5HoB7x+xX6N3Pm2eQsca+UIArsJcbuNEWWP3L51oIebnBoHJ5DTw==} peerDependencies: - '@better-auth/core': ^1.7.10 + '@better-auth/core': ^1.7.12 '@better-auth/utils': ^0.4.2 || ^0.5.0 drizzle-orm: ^0.45.2 || >=1.0.0-rc.1 <2.0.0 peerDependenciesMeta: drizzle-orm: optional: true - '@better-auth/kysely-adapter@1.7.10': - resolution: {integrity: sha512-ZWWc6h5lgcwQMTcqMLNWw+jTEMFfrqJHD+VamiCUdaAxZzVAkjLZzVA2r0vZ5w7xnTY9vWS7wuNVqAKNLRTlYQ==} + '@better-auth/kysely-adapter@1.7.12': + resolution: {integrity: sha512-1PaFXktLv4n3DmrGbPaCj5SsvcT40NnWw7NLua5/J1+8/A051N5ZzqOwJifXEIQX5k6NdwvmymwDQxKytduEAQ==} peerDependencies: - '@better-auth/core': ^1.7.10 + '@better-auth/core': ^1.7.12 '@better-auth/utils': ^0.4.2 || ^0.5.0 kysely: ^0.28.17 || ^0.29.0 peerDependenciesMeta: kysely: optional: true - '@better-auth/memory-adapter@1.7.10': - resolution: {integrity: sha512-3Di7y8BK5I2pCdpRaJ2ggkVliO/Swr6a/J+y9RFdwkbxhEMU1o9WD9IwoG3LFH8dvFF3kVo+GilsZsVNlYD+sw==} + '@better-auth/memory-adapter@1.7.12': + resolution: {integrity: sha512-glaDwXzDptnddHqRuu7fj47FAssUcOzLNgdlTGJoEPrspkX69x2vnXwgAzNuApjZI21WLtPH/rQ6CN+DQGybPA==} peerDependencies: - '@better-auth/core': ^1.7.10 + '@better-auth/core': ^1.7.12 '@better-auth/utils': ^0.4.2 || ^0.5.0 - '@better-auth/mongo-adapter@1.7.10': - resolution: {integrity: sha512-7z48PlXtlSKGvdOXfVmbjZE1vRmYyOQqHiPbeLpw+NvAPSTI9N4r7OO47u2MebA3q25e3GoGEwk5BAEE07u+PQ==} + '@better-auth/mongo-adapter@1.7.12': + resolution: {integrity: sha512-HKV4sgYSw/Gdt9AuHlESMkiKL72SWdGweDy1Wsyxoh06BpG7aNu4aSJ7marNpMdKZteyRbxWWqQkA9ediA8qFw==} peerDependencies: - '@better-auth/core': ^1.7.10 + '@better-auth/core': ^1.7.12 '@better-auth/utils': ^0.4.2 || ^0.5.0 mongodb: ^6.0.0 || ^7.0.0 peerDependenciesMeta: mongodb: optional: true - '@better-auth/prisma-adapter@1.7.10': - resolution: {integrity: sha512-hNlhn0Kcq/O76SehvqznFsjqfEMdM2m2ctazU/BLv+jT8+hGvvxCInXZi6x86/xpAyDTgU7t7KsK50r6mdNR1g==} + '@better-auth/prisma-adapter@1.7.12': + resolution: {integrity: sha512-FZRX6ORbwhDmeYE1Tl0IPtUn8v3BlVFgbJxDnm/DCNHxuFRf+0t4HfddUgYeYehHFR8aSDsdYX44fEFWM9877w==} peerDependencies: - '@better-auth/core': ^1.7.10 + '@better-auth/core': ^1.7.12 '@better-auth/utils': ^0.4.2 || ^0.5.0 '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -1097,10 +1097,10 @@ packages: prisma: optional: true - '@better-auth/telemetry@1.7.10': - resolution: {integrity: sha512-1OamUtbGRaYHB0tRjKK3Jv6hzI6mZMv3pujkg8k7tr3idQuToMoVXsWAD9oFbUTmOLeix58n+grzsl7r0fR0Sw==} + '@better-auth/telemetry@1.7.12': + resolution: {integrity: sha512-DaDkKNnI99vKGGbWq9KJg2uYeaOjsK9nIBw9T2m2KG/RkxpvQRjYkZ00++up2bZQkR5pB1nukGbplza3UJG/Xg==} peerDependencies: - '@better-auth/core': ^1.7.10 + '@better-auth/core': ^1.7.12 '@better-auth/utils': ^0.4.2 || ^0.5.0 '@better-fetch/fetch': 1.3.2 @@ -2752,10 +2752,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@opentelemetry/semantic-conventions@1.43.0': - resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} - engines: {node: '>=14'} - '@oxc-parser/binding-android-arm-eabi@0.127.0': resolution: {integrity: sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -2992,13 +2988,13 @@ packages: cpu: [x64] os: [win32] - '@pgstencil/auth@0.4.0': - resolution: {integrity: sha512-isQq1k8YxUCJyISCO7TpwGkwmfer+nGVbqsIZbrD2Na8G59B20p5bU+IL09z7tbVL80THJdD9p+PEyE0R1FuXA==} + '@pgstencil/auth@0.4.1': + resolution: {integrity: sha512-57R1AeyF6OE6OiJjWt37KFb8LYoVYcWlHKQS1u0mnuSU1BEIptmnZpYb6rfXW5CtbGzXmyxo7u4Q8z9LkP/hDA==} engines: {node: '>=24'} peerDependencies: hono: ^4.13.7 kysely: ^0.29.5 - pgstencil: ^0.4.0 + pgstencil: ^0.4.1 '@phosphor-icons/react@2.1.10': resolution: {integrity: sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA==} @@ -4680,8 +4676,8 @@ packages: bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} - better-auth@1.7.10: - resolution: {integrity: sha512-Paf0X8cHKQc8jCgM3vl4AoxRicWdNf0zw8Aggn1q6aNSWCDDxSZCb6rkXa/O9cPc8idjVPK0rV/eHPHAbs07ZA==} + better-auth@1.7.12: + resolution: {integrity: sha512-f1fEG0QdTslzbmFA+vOGFFW/XXCWkG4kBxLeH9n1vp1otTH9+ZzV6EJ6797GStWeiYYgS5Vnt9xqE12KwSyAAw==} peerDependencies: '@lynx-js/react': '*' '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 @@ -4739,8 +4735,8 @@ packages: vue: optional: true - better-call@1.4.2: - resolution: {integrity: sha512-lOmFCTG1KcE60YWxoff2Wx0Fv7v7drP/fQTDYnhJUCdkA7V6QWXdLPUiMpOQCq7Fd0Ts3AkrRwD23xoiootlGA==} + better-call@1.4.4: + resolution: {integrity: sha512-Gq4XdGFy4dmUxFEAX2nj0ycFcFbD9ZWh8YROouZoyXeF1M11TpYbplk0pDByTFF8Wz+MYa6W9052c9m7dD+mfw==} peerDependencies: zod: ^4.0.0 peerDependenciesMeta: @@ -5767,8 +5763,8 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - jose@6.2.12: - resolution: {integrity: sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==} + jose@6.2.13: + resolution: {integrity: sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==} jpeg-js@0.4.4: resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==} @@ -6729,8 +6725,8 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} - pgstencil@0.4.0: - resolution: {integrity: sha512-81Y6LTzIc1RkOh5eiRmPXuwVTR0s8+aB6DGmI/TE08LzRfozskVzaKdK3Cd6++gNPp6GsEqG9yFSFNd3unUmYw==} + pgstencil@0.4.1: + resolution: {integrity: sha512-fQJ1p4VFONFPOC8myjHJiEus0GUpH3uBFvteaoIaO6ko6pHuEP4rJBESq+e8fxV4XUXJuRecVJzpJ2AoKCvTRw==} engines: {node: '>=24'} peerDependencies: cheerio: ^1.2.0 @@ -8174,48 +8170,47 @@ snapshots: '@balena/dockerignore@1.0.2': {} - '@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3)': + '@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3)': dependencies: '@better-auth/utils': 0.5.0 '@better-fetch/fetch': 1.3.2 - '@opentelemetry/semantic-conventions': 1.43.0 '@standard-schema/spec': 1.1.0 - better-call: 1.4.2(zod@4.6.5) - jose: 6.2.12 + better-call: 1.4.4(zod@4.6.5) + jose: 6.2.13 kysely: 0.29.7 nanostores: 1.5.3 zod: 4.6.5 - '@better-auth/drizzle-adapter@1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)': + '@better-auth/drizzle-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)': dependencies: - '@better-auth/core': 1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3) + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3) '@better-auth/utils': 0.5.0 - '@better-auth/kysely-adapter@1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)(kysely@0.29.7)': + '@better-auth/kysely-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)(kysely@0.29.7)': dependencies: - '@better-auth/core': 1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3) + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3) '@better-auth/utils': 0.5.0 optionalDependencies: kysely: 0.29.7 - '@better-auth/memory-adapter@1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)': + '@better-auth/memory-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)': dependencies: - '@better-auth/core': 1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3) + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3) '@better-auth/utils': 0.5.0 - '@better-auth/mongo-adapter@1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)': + '@better-auth/mongo-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)': dependencies: - '@better-auth/core': 1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3) + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3) '@better-auth/utils': 0.5.0 - '@better-auth/prisma-adapter@1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)': + '@better-auth/prisma-adapter@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)': dependencies: - '@better-auth/core': 1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3) + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3) '@better-auth/utils': 0.5.0 - '@better-auth/telemetry@1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)': + '@better-auth/telemetry@1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)': dependencies: - '@better-auth/core': 1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3) + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3) '@better-auth/utils': 0.5.0 '@better-fetch/fetch': 1.3.2 @@ -9828,8 +9823,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@opentelemetry/semantic-conventions@1.43.0': {} - '@oxc-parser/binding-android-arm-eabi@0.127.0': optional: true @@ -9959,14 +9952,14 @@ snapshots: '@oxc-resolver/binding-win32-x64-msvc@11.21.2': optional: true - '@pgstencil/auth@0.4.0(hono@4.13.14)(kysely@0.29.7)(pg@8.23.1)(pgstencil@0.4.0(cheerio@1.2.0)(kysely@0.29.7)(node-pg-migrate@9.0.0(pg@8.23.1))(testcontainers@12.2.1(supports-color@10.2.2))(turndown@7.2.4))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5)': + '@pgstencil/auth@0.4.1(hono@4.13.14)(kysely@0.29.7)(pg@8.23.1)(pgstencil@0.4.1(cheerio@1.2.0)(kysely@0.29.7)(node-pg-migrate@9.0.0(pg@8.23.1))(testcontainers@12.2.1(supports-color@10.2.2))(turndown@7.2.4))(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5)': dependencies: - better-auth: 1.7.10(pg@8.23.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5) + better-auth: 1.7.12(pg@8.23.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5) hono: 4.13.14 - jose: 6.2.12 + jose: 6.2.13 kysely: 0.29.7 openid-client: 6.8.8 - pgstencil: 0.4.0(cheerio@1.2.0)(kysely@0.29.7)(node-pg-migrate@9.0.0(pg@8.23.1))(testcontainers@12.2.1(supports-color@10.2.2))(turndown@7.2.4) + pgstencil: 0.4.1(cheerio@1.2.0)(kysely@0.29.7)(node-pg-migrate@9.0.0(pg@8.23.1))(testcontainers@12.2.1(supports-color@10.2.2))(turndown@7.2.4) transitivePeerDependencies: - '@lynx-js/react' - '@opentelemetry/api' @@ -11523,22 +11516,22 @@ snapshots: dependencies: tweetnacl: 0.14.5 - better-auth@1.7.10(pg@8.23.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5): + better-auth@1.7.12(pg@8.23.1)(react-dom@19.3.1(react@19.3.1))(react@19.3.1)(vitest@5.0.5): dependencies: - '@better-auth/core': 1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3) - '@better-auth/drizzle-adapter': 1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0) - '@better-auth/kysely-adapter': 1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)(kysely@0.29.7) - '@better-auth/memory-adapter': 1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0) - '@better-auth/mongo-adapter': 1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0) - '@better-auth/prisma-adapter': 1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0) - '@better-auth/telemetry': 1.7.10(@better-auth/core@1.7.10(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.2(zod@4.6.5))(jose@6.2.12)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2) + '@better-auth/core': 1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3) + '@better-auth/drizzle-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0) + '@better-auth/kysely-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)(kysely@0.29.7) + '@better-auth/memory-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0) + '@better-auth/mongo-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0) + '@better-auth/prisma-adapter': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0) + '@better-auth/telemetry': 1.7.12(@better-auth/core@1.7.12(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2)(better-call@1.4.4(zod@4.6.5))(jose@6.2.13)(kysely@0.29.7)(nanostores@1.5.3))(@better-auth/utils@0.5.0)(@better-fetch/fetch@1.3.2) '@better-auth/utils': 0.5.0 '@better-fetch/fetch': 1.3.2 '@noble/ciphers': 2.4.0 '@noble/hashes': 2.4.0 - better-call: 1.4.2(zod@4.6.5) + better-call: 1.4.4(zod@4.6.5) defu: 6.1.7 - jose: 6.2.12 + jose: 6.2.13 kysely: 0.29.7 nanostores: 1.5.3 zod: 4.6.5 @@ -11550,7 +11543,7 @@ snapshots: transitivePeerDependencies: - '@opentelemetry/api' - better-call@1.4.2(zod@4.6.5): + better-call@1.4.4(zod@4.6.5): dependencies: '@better-auth/utils': 0.5.0 '@better-fetch/fetch': 1.3.2 @@ -12593,7 +12586,7 @@ snapshots: jiti@2.7.0: {} - jose@6.2.12: {} + jose@6.2.13: {} jpeg-js@0.4.4: {} @@ -13577,7 +13570,7 @@ snapshots: openid-client@6.8.8: dependencies: - jose: 6.2.12 + jose: 6.2.13 oauth4webapi: 3.8.8 ora@9.4.1: @@ -13750,7 +13743,7 @@ snapshots: dependencies: split2: 4.2.0 - pgstencil@0.4.0(cheerio@1.2.0)(kysely@0.29.7)(node-pg-migrate@9.0.0(pg@8.23.1))(testcontainers@12.2.1(supports-color@10.2.2))(turndown@7.2.4): + pgstencil@0.4.1(cheerio@1.2.0)(kysely@0.29.7)(node-pg-migrate@9.0.0(pg@8.23.1))(testcontainers@12.2.1(supports-color@10.2.2))(turndown@7.2.4): dependencies: kysely: 0.29.7 pg: 8.23.1 diff --git a/website/scripts/product-sections.test.js b/website/scripts/product-sections.test.js index 4228a21f3..b80c8fdb4 100644 --- a/website/scripts/product-sections.test.js +++ b/website/scripts/product-sections.test.js @@ -66,7 +66,7 @@ describe('the committed npm disclosure', () => { for (const name of [ '@better-auth/core', '@better-auth/kysely-adapter', '@better-auth/memory-adapter', '@better-auth/telemetry', '@better-auth/utils', '@better-fetch/fetch', '@noble/ciphers', '@noble/hashes', - '@opentelemetry/semantic-conventions', '@pgstencil/auth', 'better-auth', 'better-call', 'defu', 'hono', 'jose', + '@pgstencil/auth', 'better-auth', 'better-call', 'defu', 'hono', 'jose', 'kysely', 'pg', 'pg-cloudflare', 'pg-connection-string', 'pg-int8', 'pg-pool', 'pg-protocol', 'pg-types', 'pgpass', 'pgstencil', 'postgres-array', 'postgres-bytea', 'postgres-date', 'postgres-interval', 'rou3', 'split2', 'xtend', 'zod', diff --git a/website/src/data/dependencies-npm.json b/website/src/data/dependencies-npm.json index cb61b1af1..d5b5214a3 100644 --- a/website/src/data/dependencies-npm.json +++ b/website/src/data/dependencies-npm.json @@ -3990,7 +3990,7 @@ "author": "Bereket Engida", "homepage": "https://www.better-auth.com", "versions": [ - "1.7.10" + "1.7.12" ] }, { @@ -3999,7 +3999,7 @@ "author": "Bereket Engida", "homepage": "https://www.better-auth.com/docs/adapters/drizzle", "versions": [ - "1.7.10" + "1.7.12" ] }, { @@ -4008,7 +4008,7 @@ "author": "Bereket Engida", "homepage": "https://www.better-auth.com/docs/adapters/kysely", "versions": [ - "1.7.10" + "1.7.12" ] }, { @@ -4017,7 +4017,7 @@ "author": "Bereket Engida", "homepage": "https://www.better-auth.com", "versions": [ - "1.7.10" + "1.7.12" ] }, { @@ -4026,7 +4026,7 @@ "author": "Bereket Engida", "homepage": "https://www.better-auth.com/docs/adapters/mongodb", "versions": [ - "1.7.10" + "1.7.12" ] }, { @@ -4035,7 +4035,7 @@ "author": "Bereket Engida", "homepage": "https://www.better-auth.com/docs/adapters/prisma", "versions": [ - "1.7.10" + "1.7.12" ] }, { @@ -4044,7 +4044,7 @@ "author": "Bereket Engida", "homepage": "https://www.better-auth.com", "versions": [ - "1.7.10" + "1.7.12" ] }, { @@ -4092,22 +4092,13 @@ "2.4.0" ] }, - { - "name": "@opentelemetry/semantic-conventions", - "license": "Apache-2.0", - "author": "OpenTelemetry Authors", - "homepage": "https://github.com/open-telemetry/opentelemetry-js/tree/main/semantic-conventions", - "versions": [ - "1.43.0" - ] - }, { "name": "@pgstencil/auth", "license": "MIT", "author": "DiffPlug", "homepage": "https://github.com/diffplug/pgstencil#readme", "versions": [ - "0.4.0" + "0.4.1" ] }, { @@ -4125,7 +4116,7 @@ "author": "Bereket Engida", "homepage": "https://better-auth.com", "versions": [ - "1.7.10" + "1.7.12" ] }, { @@ -4134,7 +4125,7 @@ "author": "Bereket Engida", "homepage": "https://github.com/better-auth/better-call", "versions": [ - "1.4.2" + "1.4.4" ] }, { @@ -4152,7 +4143,7 @@ "author": "Filip Skokan ", "homepage": "https://github.com/panva/jose", "versions": [ - "6.2.12" + "6.2.13" ] }, { @@ -4269,7 +4260,7 @@ "author": "DiffPlug", "homepage": "https://github.com/diffplug/pgstencil#readme", "versions": [ - "0.4.0" + "0.4.1" ] }, { diff --git a/website/src/pages/Privacy.tsx b/website/src/pages/Privacy.tsx index d2ce50416..e6f3b0550 100644 --- a/website/src/pages/Privacy.tsx +++ b/website/src/pages/Privacy.tsx @@ -51,7 +51,7 @@ export const PRIVACY_SECTIONS: PolicySection[] = [

We use this information to create and recognize your account, verify sign-in, connect methods you explicitly select, send requested sign-in codes, check on each request that your subscription is active, deliver the features you pay for, apply fair-use limits, prevent abuse, troubleshoot failures, and answer support requests. We discard provider access, refresh, and identity tokens after identity verification rather than storing them in your account.

Every request to Hosted carries your IP address and connection information, which we and our infrastructure providers use to deliver the request, secure the service, and limit request rates. Rate-limit records we store hold a keyed hash of the address, not the address itself, and are kept only briefly.

DiffPlug does not sell Hosted information, use it for targeted advertising, or use it to train AI models. The Natural Voice section describes ElevenLabs’ separate processing. Signing in or subscribing does not subscribe you to a newsletter. The account site uses necessary authentication and security cookies and does not load marketing analytics.

-

To see whether Hosted is working, we keep aggregate daily counts on our own servers, with no per-person analytics: computers approved and enrolled, Pocket sign-ins, push notifications delivered, Natural Voice requests answered, over the daily limit, or failed, and accounts that reached the daily voice limit. A count row holds only the day, what was counted, a fixed label (such as “voice” or “over the limit”), and the number; it carries no account id, email address, IP address, browser information, or text.

+

To see whether Hosted is working, we keep aggregate daily counts on our own servers, with no per-person analytics: accounts created, account sign-ins by method (an emailed code or which provider), computers approved and enrolled, Pocket sign-ins, push notifications delivered, Natural Voice requests answered, over the daily limit, or failed, and accounts that reached the daily voice limit. A count row holds only the day, what was counted, a fixed label (such as “github” or “over the limit”), and the number; it carries no account id, email address, IP address, browser information, or text.

This website, dormouse.sh, counts its visits with PostHog. When a page loads, your browser sends PostHog, through dormouse.sh, the page’s address and title, the page that linked you there, and your browser, device type, language, time zone, and screen and window sizes. Links to the Hosted page from Dormouse or this site carry a short label naming which link it was (such as the voice setting or the tutorial); the page removes it from the address bar and adds it to that page’s visit. Clicking a plan’s buy button sends which plan, and finishing the playground tutorial sends which tutorial. Each request also carries your IP address and browser user agent. PostHog counts unique visitors without storing anything on your device: in its words, it uses “a privacy-preserving hash, calculated on PostHog’s servers” from your IP address, user agent, and this site with a random value that changes daily and is deleted once that day’s events are processed. It removes the IP address before storing the visit and does not look up your location. Nothing is kept in cookies or browser storage: PostHog only checks that storage works, with a test value it removes at once. No profile is built, and we never tell PostHog who you are. If your browser sends Global Privacy Control or Do Not Track, the site does not load PostHog at all. The Hosted account site, the Hosted Relay and Pocket, and Dormouse itself do not use PostHog.

}, { id: "legal-bases", title: "Our legal bases", body: <>