{"_id":"@clammet/convex-googly-auth","_rev":"4-5260adf22aba253b2f38a02f52932285","name":"@clammet/convex-googly-auth","dist-tags":{"latest":"1.0.1"},"versions":{"0.1.0":{"name":"@clammet/convex-googly-auth","version":"0.1.0","keywords":["convex","component","auth","google","anonymous"],"license":"Apache-2.0","_id":"@clammet/convex-googly-auth@0.1.0","maintainers":[{"name":"clammet","email":"npm@nyanya.org"}],"homepage":"https://github.com/clammet/convex-googly-auth#readme","bugs":{"url":"https://github.com/clammet/convex-googly-auth/issues"},"dist":{"shasum":"4bc15ec4f1792cd1df279998fa1c543b585f1b7c","tarball":"https://registry.npmjs.org/@clammet/convex-googly-auth/-/convex-googly-auth-0.1.0.tgz","fileCount":60,"integrity":"sha512-Wef/8N86SXaDRO952eeiOU4SkRLkDdumCO9cgcJcOJkw+on98H7k+ixm2pfGDdXhkpyrwoSV9ckj+Va7cNFxig==","signatures":[{"sig":"MEUCIDl79oiNE0EZHPrsQfxXgMD61E/NuWl5RYwsu+/3mxGWAiEA8gkI9zqVuILD1caAhPsErbaaOZ4kjtGxbJiFTrP/QGM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":205291},"type":"module","types":"./dist/client/index.d.ts","module":"./dist/client/index.js","exports":{".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./test":"./src/test.ts","./react":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"./package.json":"./package.json","./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./_generated/component":{"types":"./dist/component/_generated/component.d.ts"},"./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"}},"gitHead":"636f54969694caed8e59a8e0cfe0bd9dbc81c843","scripts":{"dev":"convex dev --start 'npm run dev:build'","lint":"eslint .","test":"vitest run --typecheck","build":"tsc --project ./tsconfig.build.json","check":"npm run build && npm run test && npm run typecheck && npm run lint && npm run check:package","predev":"convex init && npm run build:codegen","prepare":"npm run build","release":"changeset publish","changeset":"changeset","dev:build":"chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial","typecheck":"tsc --noEmit && tsc -p example && tsc -p example/convex","test:debug":"vitest --inspect-brk --no-file-parallelism","test:watch":"vitest --typecheck --clearScreen false","build:clean":"rm -rf dist *.tsbuildinfo && npm run build:codegen","dev:frontend":"cd example && vite","build:codegen":"npx convex codegen --component-dir ./src/component && npm run build","check:package":"node scripts/check-package.mjs","test:coverage":"vitest run --coverage --coverage.reporter=text","prepublishOnly":"npm run check","version-packages":"changeset version"},"_npmUser":{"name":"clammet","email":"npm@nyanya.org"},"repository":{"url":"git+https://github.com/clammet/convex-googly-auth.git","type":"git"},"_npmVersion":"11.15.0","description":"Google OIDC + optional anonymous identity component for Convex apps.","directories":{},"_nodeVersion":"25.6.1","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"vite":"8.2.0","react":"^19.2.5","convex":"1.43.0","eslint":"9.39.4","vitest":"4.1.4","globals":"^17.5.0","prettier":"3.8.3","react-dom":"^19.2.5","@eslint/js":"9.39.4","pkg-pr-new":"^0.0.66","typescript":"6.0.3","@types/node":"^24.12.2","convex-test":"0.0.49","@types/react":"^19.2.14","chokidar-cli":"3.0.0","@changesets/cli":"^2.31.1","@edge-runtime/vm":"^5.0.0","@eslint/eslintrc":"^3.3.5","@types/react-dom":"^19.2.3","typescript-eslint":"8.58.2","eslint-plugin-react":"^7.37.5","@vitejs/plugin-react":"^6.0.2","@convex-dev/eslint-plugin":"^2.0.0","eslint-plugin-react-hooks":"^7.1.1","eslint-plugin-react-refresh":"^0.5.2"},"peerDependencies":{"react":"^18.3.1 || ^19.0.0","convex":"^1.43.0"},"_npmOperationalInternal":{"tmp":"tmp/convex-googly-auth_0.1.0_1786198512788_0.7501358269311895","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@clammet/convex-googly-auth","version":"0.1.1","keywords":["convex","component","auth","google","anonymous"],"license":"Unlicense","_id":"@clammet/convex-googly-auth@0.1.1","maintainers":[{"name":"clammet","email":"npm@nyanya.org"}],"homepage":"https://github.com/clammet/convex-googly-auth#readme","bugs":{"url":"https://github.com/clammet/convex-googly-auth/issues"},"dist":{"shasum":"af6ea6b6f114e504318be28e80022f09fa974e8f","tarball":"https://registry.npmjs.org/@clammet/convex-googly-auth/-/convex-googly-auth-0.1.1.tgz","fileCount":60,"integrity":"sha512-owpIEOxWXWwQI5oHwK6L0NWVOTIsKaeNmoyLKYD9RvLrsDf/ZN8SiIoAm4lUq9vZcjCdwEzpuxN7prQmxx+l7A==","signatures":[{"sig":"MEYCIQDW8J2Yvg/Sb6WP2tLhPUSx1lHO8pb2SdlmX0RrqxUG5QIhAOFX3WkkVjtDsewdibYWHpTKZi1k5hqb/4ZFFlvay+/S","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@clammet%2fconvex-googly-auth@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":195490},"type":"module","types":"./dist/client/index.d.ts","module":"./dist/client/index.js","exports":{".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./test":"./src/test.ts","./react":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"./package.json":"./package.json","./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./_generated/component":{"types":"./dist/component/_generated/component.d.ts"},"./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"}},"gitHead":"577ef1d6e2a69c514a733f005162dbeaafec0aa0","scripts":{"dev":"convex dev --start 'npm run dev:build'","lint":"eslint .","test":"vitest run --typecheck","build":"tsc --project ./tsconfig.build.json","check":"npm run build && npm run test && npm run typecheck && npm run lint && npm run check:package","predev":"convex init && npm run build:codegen","prepare":"npm run build","release":"changeset publish","changeset":"changeset","dev:build":"chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial","typecheck":"tsc --noEmit && tsc -p example && tsc -p example/convex","test:debug":"vitest --inspect-brk --no-file-parallelism","test:watch":"vitest --typecheck --clearScreen false","build:clean":"rm -rf dist *.tsbuildinfo && npm run build:codegen","dev:frontend":"cd example && vite","build:codegen":"npx convex codegen --component-dir ./src/component && npm run build","check:package":"node scripts/check-package.mjs","test:coverage":"vitest run --coverage --coverage.reporter=text","prepublishOnly":"npm run check","version-packages":"changeset version"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:3b8a87f6-0a42-408c-a454-2195a049d700"}},"repository":{"url":"git+https://github.com/clammet/convex-googly-auth.git","type":"git"},"_npmVersion":"11.17.0","description":"Google OIDC + optional anonymous identity component for Convex apps.","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"vite":"8.2.0","react":"^19.2.5","convex":"1.43.0","eslint":"9.39.4","vitest":"4.1.4","globals":"^17.5.0","prettier":"3.8.3","react-dom":"^19.2.5","@eslint/js":"9.39.4","pkg-pr-new":"^0.0.66","typescript":"6.0.3","@types/node":"^24.12.2","convex-test":"0.0.49","@types/react":"^19.2.14","chokidar-cli":"3.0.0","@changesets/cli":"^2.31.1","@edge-runtime/vm":"^5.0.0","@eslint/eslintrc":"^3.3.5","@types/react-dom":"^19.2.3","typescript-eslint":"8.58.2","eslint-plugin-react":"^7.37.5","@vitejs/plugin-react":"^6.0.2","@convex-dev/eslint-plugin":"^2.0.0","eslint-plugin-react-hooks":"^7.1.1","eslint-plugin-react-refresh":"^0.5.2"},"peerDependencies":{"react":"^18.3.1 || ^19.0.0","convex":"^1.43.0"},"_npmOperationalInternal":{"tmp":"tmp/convex-googly-auth_0.1.1_1787828993674_0.920246409813769","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@clammet/convex-googly-auth","version":"1.0.0","keywords":["convex","component","auth","google","anonymous"],"license":"Unlicense","_id":"@clammet/convex-googly-auth@1.0.0","maintainers":[{"name":"clammet","email":"npm@nyanya.org"}],"homepage":"https://github.com/clammet/convex-googly-auth#readme","bugs":{"url":"https://github.com/clammet/convex-googly-auth/issues"},"dist":{"shasum":"a46254749956702711b67949bcdb708f2e78f6d4","tarball":"https://registry.npmjs.org/@clammet/convex-googly-auth/-/convex-googly-auth-1.0.0.tgz","fileCount":60,"integrity":"sha512-uaG2aZ2yH/GkfCGAQwcAjTmQ5AFP2ltpofYiq0vwUWXd5LTuOV+/Em7gpOMCsryVNL5i8yEuOfzMwcdJ9SfZ8g==","signatures":[{"sig":"MEUCIGYWNGUPLaa6l7atocbbRpZo5UQtPr6LeMMrcsHamuAVAiEA6c5+msGVxjdEIUeHka7Y8/KnQc8HfkEVuaNQjm2gh/Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@clammet%2fconvex-googly-auth@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":195490},"type":"module","types":"./dist/client/index.d.ts","module":"./dist/client/index.js","exports":{".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./test":"./src/test.ts","./react":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"./package.json":"./package.json","./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./_generated/component":{"types":"./dist/component/_generated/component.d.ts"},"./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"}},"gitHead":"840065502bf411d7e2258b252eed03ca94a830bb","scripts":{"dev":"convex dev --start 'npm run dev:build'","lint":"eslint .","test":"vitest run --typecheck","build":"tsc --project ./tsconfig.build.json","check":"npm run build && npm run test && npm run typecheck && npm run lint && npm run check:package","predev":"convex init && npm run build:codegen","prepare":"npm run build","release":"changeset publish","changeset":"changeset","dev:build":"chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial","typecheck":"tsc --noEmit && tsc -p example && tsc -p example/convex","test:debug":"vitest --inspect-brk --no-file-parallelism","test:watch":"vitest --typecheck --clearScreen false","build:clean":"rm -rf dist *.tsbuildinfo && npm run build:codegen","dev:frontend":"cd example && vite","build:codegen":"npx convex codegen --component-dir ./src/component && npm run build","check:package":"node scripts/check-package.mjs","test:coverage":"vitest run --coverage --coverage.reporter=text","prepublishOnly":"npm run check","version-packages":"changeset version"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:3b8a87f6-0a42-408c-a454-2195a049d700"}},"repository":{"url":"git+https://github.com/clammet/convex-googly-auth.git","type":"git"},"_npmVersion":"11.17.0","description":"Google OIDC + optional anonymous identity component for Convex apps.","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"vite":"8.2.0","react":"^19.2.5","convex":"1.43.0","eslint":"9.39.4","vitest":"4.1.4","globals":"^17.5.0","prettier":"3.8.3","react-dom":"^19.2.5","@eslint/js":"9.39.4","pkg-pr-new":"^0.0.66","typescript":"6.0.3","@types/node":"^24.12.2","convex-test":"0.0.49","@types/react":"^19.2.14","chokidar-cli":"3.0.0","@changesets/cli":"^2.31.1","@edge-runtime/vm":"^5.0.0","@eslint/eslintrc":"^3.3.5","@types/react-dom":"^19.2.3","typescript-eslint":"8.58.2","eslint-plugin-react":"^7.37.5","@vitejs/plugin-react":"^6.0.2","@convex-dev/eslint-plugin":"^2.0.0","eslint-plugin-react-hooks":"^7.1.1","eslint-plugin-react-refresh":"^0.5.2"},"peerDependencies":{"react":"^18.3.1 || ^19.0.0","convex":"^1.43.0"},"_npmOperationalInternal":{"tmp":"tmp/convex-googly-auth_1.0.0_1787830348940_0.8863690662564954","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@clammet/convex-googly-auth","description":"Google OIDC + optional anonymous identity component for Convex apps.","repository":{"type":"git","url":"git+https://github.com/clammet/convex-googly-auth.git"},"homepage":"https://github.com/clammet/convex-googly-auth#readme","bugs":{"url":"https://github.com/clammet/convex-googly-auth/issues"},"version":"1.0.1","license":"Unlicense","keywords":["convex","component","auth","google","anonymous"],"type":"module","scripts":{"dev":"convex dev --start 'npm run dev:build'","dev:frontend":"cd example && vite","dev:build":"chokidar 'tsconfig*.json' 'src/**/*.ts' -i '**/*.test.ts' -c 'npm run build:codegen' --initial","predev":"convex init && npm run build:codegen","build":"tsc --project ./tsconfig.build.json","build:codegen":"npx convex codegen --component-dir ./src/component && npm run build","build:clean":"rm -rf dist *.tsbuildinfo && npm run build:codegen","check":"npm run build && npm run test && npm run typecheck && npm run lint && npm run check:package","check:package":"node scripts/check-package.mjs","typecheck":"tsc --noEmit && tsc -p example && tsc -p example/convex","lint":"eslint .","test":"vitest run --typecheck","test:watch":"vitest --typecheck --clearScreen false","test:debug":"vitest --inspect-brk --no-file-parallelism","test:coverage":"vitest run --coverage --coverage.reporter=text","changeset":"changeset","version-packages":"changeset version","release":"changeset publish","prepare":"npm run build","prepublishOnly":"npm run check"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org/"},"exports":{"./package.json":"./package.json",".":{"types":"./dist/client/index.d.ts","default":"./dist/client/index.js"},"./react":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"./test":"./src/test.ts","./_generated/component.js":{"types":"./dist/component/_generated/component.d.ts"},"./_generated/component":{"types":"./dist/component/_generated/component.d.ts"},"./convex.config.js":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"},"./convex.config":{"types":"./dist/component/convex.config.d.ts","default":"./dist/component/convex.config.js"}},"peerDependencies":{"convex":"^1.43.0","react":"^18.3.1 || ^19.0.0"},"devDependencies":{"@changesets/cli":"2.31.1","@convex-dev/eslint-plugin":"2.0.0","@edge-runtime/vm":"5.0.0","@eslint/eslintrc":"3.3.6","@eslint/js":"9.39.5","@types/node":"24.13.3","@types/react":"19.2.18","@types/react-dom":"19.2.4","@vitejs/plugin-react":"6.0.5","chokidar-cli":"3.0.0","convex":"1.45.0","convex-test":"0.0.56","eslint":"9.39.5","eslint-plugin-react":"7.37.5","eslint-plugin-react-hooks":"7.1.1","eslint-plugin-react-refresh":"0.5.3","globals":"17.9.0","pkg-pr-new":"0.0.66","prettier":"3.9.6","react":"19.2.8","react-dom":"19.2.8","typescript":"6.0.3","typescript-eslint":"8.68.0","vite":"8.2.2","vitest":"4.1.11"},"types":"./dist/client/index.d.ts","module":"./dist/client/index.js","gitHead":"ee52116c6153b60c119375907ddc15f4ca6b44ec","_id":"@clammet/convex-googly-auth@1.0.1","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-d/UdXmspETcpPt87YbHRTm7Ztv3tXJLtrjN8qxLh6KSEA1ghLJlreL6Wp6yP0LhgoZ5ChMnGP9c/4/X3aTQawg==","shasum":"16f728694e4948890eff29b86fbe9f18f8f24369","tarball":"https://registry.npmjs.org/@clammet/convex-googly-auth/-/convex-googly-auth-1.0.1.tgz","fileCount":60,"unpackedSize":195476,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@clammet%2fconvex-googly-auth@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHVO9uY4W1Ou6qCE/bWSYXiicWLe87XTFYSQxiexuhaZAiAidKY4jnLnczk98pS6nWN7nCg6bKvOJSHNZK/foyPgnw=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:3b8a87f6-0a42-408c-a454-2195a049d700"}},"directories":{},"maintainers":[{"name":"clammet","email":"npm@nyanya.org"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/convex-googly-auth_1.0.1_1787870611301_0.9222522157598334"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-08T14:15:12.683Z","modified":"2026-08-27T22:43:31.952Z","0.1.0":"2026-08-08T14:15:12.937Z","0.1.1":"2026-08-27T11:09:53.840Z","1.0.0":"2026-08-27T11:32:29.085Z","1.0.1":"2026-08-27T22:43:31.502Z"},"bugs":{"url":"https://github.com/clammet/convex-googly-auth/issues"},"license":"Unlicense","homepage":"https://github.com/clammet/convex-googly-auth#readme","keywords":["convex","component","auth","google","anonymous"],"repository":{"type":"git","url":"git+https://github.com/clammet/convex-googly-auth.git"},"description":"Google OIDC + optional anonymous identity component for Convex apps.","maintainers":[{"name":"clammet","email":"npm@nyanya.org"}],"readme":"# convex-googly-auth\n\nA [Convex component](https://docs.convex.dev/components) providing the shared\n\"easy anonymous / Google account\" identity scheme used by `upgallery` and\n`when`: Google OIDC sign-in with server-held refresh tokens, plus optional\nanonymous identities backed by a hashed bearer claim — with the\nanonymous-credential-hijacking bug class fixed structurally, in one place.\n\n## Security model\n\n- **Google sign-in.** Google issues the ID token; Convex verifies issuer,\n  audience, and signature via the app's `convex/auth.config.ts`. Refresh\n  tokens never leave the component's `authSessions` table — the browser only\n  holds an opaque HMAC-signed session token and exchanges it for fresh ID\n  tokens through the refresh route. Every refresh rotates the session token\n  (with a short grace window for racing tabs), so an exfiltrated token goes\n  stale on the next refresh. Sessions expire after 180 days absolute /\n  60 days idle by default; configure via `sessionAbsoluteTtlMs` /\n  `sessionIdleTtlMs` on `new GooglyAuth(...)`. Sign-in uses\n  `prompt=select_account`; the callback re-runs the flow with\n  `prompt=consent` only when a refresh token is needed and none is stored.\n- **Anonymous users (optional).** Identified by a 256-bit hex claim in a\n  cookie. The claim is a bearer secret; only its SHA-256 hash is stored.\n- **Structural hijack prevention.** Credentials live in their own tables\n  (`googleCredentials`, `anonymousCredentials`) pointing at an `identities`\n  row. Upgrading an anonymous account to Google sign-in *deletes* the\n  anonymous credential row, so a retired claim cannot remain a second,\n  password-less way into the account — the \"profile carrying both\n  identifiers\" shape behind the original `anonymousId` hijacking bugs is\n  unrepresentable, and a defensive guard retires legacy dual-credential rows\n  on sight. The regression suite in `example/convex/auth.test.ts` and\n  `src/component/lib.test.ts` pins all of this down.\n- **Apps never see credentials.** The component hands the app an opaque\n  `identityId` string; the app keys its own profile table by it. In-place\n  upgrades keep the same `identityId`, so app data does not move when a user\n  signs in.\n\n## Installation\n\n```bash\nnpm install @clammet/convex-googly-auth\n```\n\n```ts\n// convex/convex.config.ts\nimport { defineApp } from \"convex/server\";\nimport googlyAuth from \"@clammet/convex-googly-auth/convex.config.js\";\n\nconst app = defineApp();\napp.use(googlyAuth);\nexport default app;\n```\n\n```ts\n// convex/auth.config.ts\nexport default {\n  providers: [\n    {\n      domain: \"https://accounts.google.com\",\n      applicationID: process.env.AUTH_GOOGLE_ID,\n    },\n  ],\n};\n```\n\nEnvironment variables on the deployment: `AUTH_GOOGLE_ID`,\n`AUTH_GOOGLE_SECRET`, `SITE_URL` (canonical web origin).\n\n## Server usage\n\n```ts\n// convex/lib/auth.ts — one instance for the whole app\nimport { GooglyAuth } from \"@clammet/convex-googly-auth\";\nimport { components } from \"../_generated/api\";\n\nexport const googly = new GooglyAuth(components.googlyAuth);\n// Google-only app? Anonymous claims are then ignored server-side everywhere:\n// export const googly = new GooglyAuth(components.googlyAuth, { anonymous: false });\n```\n\n```ts\n// convex/profiles.ts — the app owns its profile table\nexport const ensureCurrent = mutation({\n  args: { anonymousClaim: v.optional(v.string()) },\n  handler: async (ctx, args) => {\n    const result = await googly.ensureIdentity(ctx, args);\n    // result.identityId  — key your profile row by this (index it!)\n    // result.identity    — the verified Google UserIdentity, or null\n    // result.upgraded    — anonymous identity gained Google sign-in in place\n    // result.mergedFromId — an anonymous identity was absorbed into this\n    //                       one; move or alias your rows keyed by it\n    ...\n  },\n});\n\n// In queries/mutations that need the caller:\nconst identityId = await googly.resolveIdentity(ctx, {\n  anonymousClaim: args.anonymousClaim,\n});\n```\n\n```ts\n// convex/http.ts\nconst http = httpRouter();\ngoogly.registerRoutes(http, {\n  // All optional; defaults read AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET / SITE_URL.\n  allowedOrigins: [\"https://alt.example.com\"],\n  // Or check a table of custom domains:\n  // isAllowedOrigin: async (ctx, origin) => { ... },\n});\nexport default http;\n```\n\nRoutes mounted (prefix configurable via `pathPrefix`): `GET /auth/google/start`,\n`GET /auth/google/callback`, `POST /auth/refresh`, `POST /auth/sign-out`.\nThe callback redirects to `{origin}/auth/callback` on the web app.\n\nOptionally add a cron calling `googly.cleanupExpiredSessions(ctx)`.\n\n## React usage\n\n```ts\n// src/lib/authClient.ts\nimport { createGooglyAuthClient } from \"@clammet/convex-googly-auth/react\";\n\nexport const authClient = createGooglyAuthClient({\n  convexSiteUrl: import.meta.env.VITE_CONVEX_SITE_URL,\n  googleClientId: import.meta.env.VITE_GOOGLE_CLIENT_ID,\n  storagePrefix: \"myapp\",       // localStorage keys + claim cookie name\n  // anonymous: false,          // must match the server-side option\n});\n```\n\n```tsx\n// main.tsx\n<authClient.GoogleAuthProvider>\n  <ConvexProviderWithAuth client={convex} useAuth={authClient.useConvexGooglyAuth}>\n    <App />\n  </ConvexProviderWithAuth>\n</authClient.GoogleAuthProvider>\n```\n\n- `authClient.useGoogleAuth()` → `{ isLoading, isAuthenticated, signIn, signOut }`\n- `authClient.useAnonymousClaim()` → the claim to pass to Convex functions\n  (null in Google-only mode)\n- On your `/auth/callback` page call `authClient.handleAuthCallback()` once\n  and navigate to the returned `redirect` (see `example/src/App.tsx`)\n- After a signed-in `ensure` succeeds, call `authClient.clearAnonymousClaim()`\n\nThe `example/` directory is a complete working app; run tests with\n`npm test`. Consumers can register the component in their own `convex-test`\nsuites via `@clammet/convex-googly-auth/test` (see\n`example/convex/setup.test.ts`).\n\n## Migrating upgallery / when onto this component\n\nBoth apps keep their profile tables and authorization logic; only the\ncredential columns move into the component.\n\n1. Add `identityId: v.string()` (indexed) to the profile table; keep\n   app-only fields (display name, roles, timezone, ...).\n2. Backfill: for each profile row, insert component rows —\n   `googleSubject` → a `googleCredentials` row, live `anonymousClaimHash` /\n   `anonymousId` → an `anonymousCredentials` row (hash `when`'s plaintext\n   ids with SHA-256 → base64url first; **skip any anonymous credential on a\n   row that also has a Google subject** — those are exactly the hijackable\n   legacy rows). Store the returned identity id in `identityId`.\n3. Replace `getCurrentProfile`-style helpers with\n   `googly.resolveIdentity(...)` + a `by_identityId` profile lookup, and the\n   profile-bootstrap/merge mutations with `googly.ensureIdentity(...)`,\n   handling `mergedFromId` with the app's existing merge strategy\n   (upgallery: alias rows; when: `moveProfileData`).\n4. Replace the hand-rolled `/auth/*` HTTP routes with\n   `googly.registerRoutes(http, ...)` (upgallery: pass an `isAllowedOrigin`\n   callback that checks `galleryHosts`), and the frontend\n   `googleAuth.tsx` / claim helpers with `createGooglyAuthClient`.\n\nNotes:\n\n- Signing keys are derived from `AUTH_GOOGLE_SECRET` plus a\n  `signingNamespace` (default `\"googly-auth\"`), so existing sessions and\n  in-flight OAuth states are invalidated at migration; users just sign in\n  again.\n- `when`'s localStorage UUID `anonymousId`s are not valid claims (claims are\n  64-hex). Either accept that pre-migration anonymous visitors get fresh\n  identities, or do a one-time in-app exchange: the server looks up the\n  legacy id, attaches a fresh claim (which the client stores) to the same\n  identity, then deletes the legacy id.\n- Requires `convex >= 1.43`.\n\n---\n\n### Repository layout\n\n- `src/component/` — the component: schema (identities, credentials,\n  sessions) and functions. Has its own `_generated/`.\n- `src/client/` — app-side `GooglyAuth` class and HTTP route registration\n  (auth + env access happen in the app, not the component).\n- `src/react/` — browser client factory.\n- `src/test.ts` — `convex-test` registration helper (exported as\n  `@clammet/convex-googly-auth/test`).\n- `example/` — working example app + the regression test suite.\n\n### Development\n\n```bash\nnpm install --ignore-scripts   # prepare runs a build; skip on first install\nnpm run build                  # or build:codegen with a configured deployment\nnpm run check\n```\n","readmeFilename":"README.md"}