@clammet/convex-googly-auth

中文文档
1.0.3发布于 2026/10/28UnlicenseTypeScriptES Module零依赖构建来源已验证近 30 天下载 2321

Google OIDC + optional anonymous identity component for Convex apps.

原始使用文档
文档目录7 节
  1. Security model
  2. Installation
  3. Server usage
  4. React usage
  5. Migrating upgallery / when onto this component
  6. Repository layout
  7. Development

convex-googly-auth

A Convex component providing the shared "easy anonymous / Google account" identity scheme used by upgallery and when: Google OIDC sign-in with server-held refresh tokens, plus optional anonymous identities backed by a hashed bearer claim — with the anonymous-credential-hijacking bug class fixed structurally, in one place.

Security model

  • Google sign-in. Google issues the ID token; Convex verifies issuer, audience, and signature via the app's convex/auth.config.ts. Refresh tokens never leave the component's authSessions table — the browser only holds an opaque HMAC-signed session token and exchanges it for fresh ID tokens through the refresh route. Every refresh rotates the session token (with a short grace window for racing tabs), so an exfiltrated token goes stale on the next refresh. Sessions expire after 180 days absolute / 60 days idle by default; configure via sessionAbsoluteTtlMs / sessionIdleTtlMs on new GooglyAuth(...). Sign-in uses prompt=select_account; the callback re-runs the flow with prompt=consent only when a refresh token is needed and none is stored.
  • Anonymous users (optional). Identified by a 256-bit hex claim in a cookie. The claim is a bearer secret; only its SHA-256 hash is stored.
  • Structural hijack prevention. Credentials live in their own tables (googleCredentials, anonymousCredentials) pointing at an identities row. Upgrading an anonymous account to Google sign-in deletes the anonymous credential row, so a retired claim cannot remain a second, password-less way into the account — the "profile carrying both identifiers" shape behind the original anonymousId hijacking bugs is unrepresentable, and a defensive guard retires legacy dual-credential rows on sight. The regression suite in example/convex/auth.test.ts and src/component/lib.test.ts pins all of this down.
  • Apps never see credentials. The component hands the app an opaque identityId string; the app keys its own profile table by it. In-place upgrades keep the same identityId, so app data does not move when a user signs in.

Installation

npm install @clammet/convex-googly-auth
// convex/convex.config.ts
import { defineApp } from "convex/server";
import googlyAuth from "@clammet/convex-googly-auth/convex.config.js";

const app = defineApp();
app.use(googlyAuth);
export default app;
// convex/auth.config.ts
export default {
  providers: [
    {
      domain: "https://accounts.google.com",
      applicationID: process.env.AUTH_GOOGLE_ID,
    },
  ],
};

Environment variables on the deployment: AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET, SITE_URL (canonical web origin).

Server usage

// convex/lib/auth.ts — one instance for the whole app
import { GooglyAuth } from "@clammet/convex-googly-auth";
import { components } from "../_generated/api";

export const googly = new GooglyAuth(components.googlyAuth);
// Google-only app? Anonymous claims are then ignored server-side everywhere:
// export const googly = new GooglyAuth(components.googlyAuth, { anonymous: false });
// convex/profiles.ts — the app owns its profile table
export const ensureCurrent = mutation({
  args: { anonymousClaim: v.optional(v.string()) },
  handler: async (ctx, args) => {
    const result = await googly.ensureIdentity(ctx, args);
    // result.identityId  — key your profile row by this (index it!)
    // result.identity    — the verified Google UserIdentity, or null
    // result.upgraded    — anonymous identity gained Google sign-in in place
    // result.mergedFromId — an anonymous identity was absorbed into this
    //                       one; move or alias your rows keyed by it
    ...
  },
});

// In queries/mutations that need the caller:
const identityId = await googly.resolveIdentity(ctx, {
  anonymousClaim: args.anonymousClaim,
});
// convex/http.ts
const http = httpRouter();
googly.registerRoutes(http, {
  // All optional; defaults read AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET / SITE_URL.
  allowedOrigins: ["https://alt.example.com"],
  // Or check a table of custom domains:
  // isAllowedOrigin: async (ctx, origin) => { ... },
});
export default http;

Routes mounted (prefix configurable via pathPrefix): GET /auth/google/start, GET /auth/google/callback, POST /auth/refresh, POST /auth/sign-out. The callback redirects to {origin}/auth/callback on the web app.

Optionally add a cron calling googly.cleanupExpiredSessions(ctx).

React usage

// src/lib/authClient.ts
import { createGooglyAuthClient } from "@clammet/convex-googly-auth/react";

export const authClient = createGooglyAuthClient({
  convexSiteUrl: import.meta.env.VITE_CONVEX_SITE_URL,
  googleClientId: import.meta.env.VITE_GOOGLE_CLIENT_ID,
  storagePrefix: "myapp",       // localStorage keys + claim cookie name
  // anonymous: false,          // must match the server-side option
});
// main.tsx
<authClient.GoogleAuthProvider>
  <ConvexProviderWithAuth client={convex} useAuth={authClient.useConvexGooglyAuth}>
    <App />
  </ConvexProviderWithAuth>
</authClient.GoogleAuthProvider>
  • authClient.useGoogleAuth() → { isLoading, isAuthenticated, signIn, signOut }
  • authClient.useAnonymousClaim() → the claim to pass to Convex functions (null in Google-only mode)
  • On your /auth/callback page call authClient.handleAuthCallback() once and navigate to the returned redirect (see example/src/App.tsx)
  • After a signed-in ensure succeeds, call authClient.clearAnonymousClaim()

The example/ directory is a complete working app; run tests with npm test. Consumers can register the component in their own convex-test suites via @clammet/convex-googly-auth/test (see example/convex/setup.test.ts).

Migrating upgallery / when onto this component

Both apps keep their profile tables and authorization logic; only the credential columns move into the component.

  1. Add identityId: v.string() (indexed) to the profile table; keep app-only fields (display name, roles, timezone, ...).
  2. Backfill: for each profile row, insert component rows — googleSubject → a googleCredentials row, live anonymousClaimHash / anonymousId → an anonymousCredentials row (hash when's plaintext ids with SHA-256 → base64url first; skip any anonymous credential on a row that also has a Google subject — those are exactly the hijackable legacy rows). Store the returned identity id in identityId.
  3. Replace getCurrentProfile-style helpers with googly.resolveIdentity(...) + a by_identityId profile lookup, and the profile-bootstrap/merge mutations with googly.ensureIdentity(...), handling mergedFromId with the app's existing merge strategy (upgallery: alias rows; when: moveProfileData).
  4. Replace the hand-rolled /auth/* HTTP routes with googly.registerRoutes(http, ...) (upgallery: pass an isAllowedOrigin callback that checks galleryHosts), and the frontend googleAuth.tsx / claim helpers with createGooglyAuthClient.

Notes:

  • Signing keys are derived from AUTH_GOOGLE_SECRET plus a signingNamespace (default "googly-auth"), so existing sessions and in-flight OAuth states are invalidated at migration; users just sign in again.
  • when's localStorage UUID anonymousIds are not valid claims (claims are 64-hex). Either accept that pre-migration anonymous visitors get fresh identities, or do a one-time in-app exchange: the server looks up the legacy id, attaches a fresh claim (which the client stores) to the same identity, then deletes the legacy id.
  • Requires convex >= 1.43.

Repository layout

  • src/component/ — the component: schema (identities, credentials, sessions) and functions. Has its own _generated/.
  • src/client/ — app-side GooglyAuth class and HTTP route registration (auth + env access happen in the app, not the component).
  • src/react/ — browser client factory.
  • src/test.ts — convex-test registration helper (exported as @clammet/convex-googly-auth/test).
  • example/ — working example app + the regression test suite.

Development

npm install --ignore-scripts   # prepare runs a build; skip on first install
npm run build                  # or build:codegen with a configured deployment
npm run check

版本与发布记录

@clammet/convex-googly-auth 共发布 6 个版本,首次发布于 2026/08/08;过去 12 个月发布了 6 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
1.0.3latest2026/10/2812 天前
1.0.22026/10/2120 天前
1.0.12026/08/282 个月前
1.0.02026/08/272 个月前
0.1.12026/08/272 个月前
0.1.02026/08/083 个月前

关于 @clammet/convex-googly-auth 的常见问题

@clammet/convex-googly-auth 是什么?

Google OIDC + optional anonymous identity component for Convex apps.

如何安装 @clammet/convex-googly-auth?

在项目目录运行 npm install @clammet/convex-googly-auth,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^18.3.1 || ^19.0.0)、convex(^1.43.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@clammet/convex-googly-auth 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@clammet/convex-googly-auth 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@clammet/convex-googly-auth 可以商用吗?

@clammet/convex-googly-auth 使用 Unlicense 许可。作者将代码贡献到公共领域,可不受限制地使用。以上为概括,不构成法律意见,请以许可原文为准。

@clammet/convex-googly-auth 的下载量有多少?

根据 npm 官方下载统计,@clammet/convex-googly-auth 在 2026/10/10 – 2026/11/08 这 30 天内被下载 2,321 次,日均约 77 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@clammet/convex-googly-auth 还在维护吗?

最新版本 1.0.3 发布于 2026/10/28(12 天前);至今共发布 6 个版本,其中过去 12 个月发布了 6 个;首次发布于 2026/08/08。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。