Repository navigation
feat: agent auth 2.0 with devices, robots, quorum approvals, core email and password, and security fixes - #273
Merged
Merged
Conversation
…d on org and API key handlers
…2.x and update the lockfile
…single use claims under load
…venance wiring and the wave 8 review fixes
… stop the approval webhook test racing
…te the llms files
…family revoke, step up follow ups and stricter device bundles
…DK and prove the docs snippets run
…list enrollment tokens and share one delegation revoke
…heir own documentation
… and a Solid client
…re contract suite, Mongo and SQL secondary storage, Supabase and WorkOS importers, Vonage and Plivo OTP senders
| } | ||
|
|
||
| export function createSessionStore(options: SessionStoreOptions = {}): SessionStore { | ||
| const base = (options.basePath ?? "/api/theauth").replace(/\/+$/, ""); |
| const BYTES_PER_CHAR = 4; | ||
|
|
||
| const CREATE_INDEX = | ||
| /^CREATE\s+(UNIQUE\s+)?INDEX\s+(?:IF NOT EXISTS\s+)?(\w+)\s+ON\s+(\w+)\s*\(([^)]*(?:\([^)]*\)[^)]*)*)\)\s*$/i; |
| export function createHttpGuard(options: HttpGuardOptions): HttpGuard { | ||
| const doFetch = options.fetch ?? fetch; | ||
| const action = options.action ?? "execute"; | ||
| const url = `${options.baseUrl.replace(/\/+$/, "")}/authorize/token`; |
| command: RobotCommand, | ||
| ): Promise<{ allowed: true; auditId?: string } | RobotGuardResult<never>> { | ||
| const doFetch = options.fetch ?? fetch; | ||
| const url = `${options.baseUrl.replace(/\/+$/, "")}/authorize/token`; |
…ure, atomic org remove, Prisma quorum, approvals CLI and client quorum fields
…on and matching, fewer queries on authorize and admin lists
…ces, declare the hot path indexes in the schemas
… scan, wait for both quorum webhooks
|
|
||
| function chainOf(input: Uint8Array[] | string): Uint8Array[] | null { | ||
| if (typeof input !== "string") return input; | ||
| const blocks = input.match(/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g); |
Comment on lines
+120
to
+122
| for (const block of pem.match( | ||
| /-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g, | ||
| ) ?? [pem]) { |
…libsql provider, id generation, Cloudflare and Paybin providers, Nitro and Waku recipes
…python and go clients for them
…te paths, fix name and metadata replacement on magic link and email otp sign-ups
…ce attestation, a kysely store kernel, and verified email linking Core email and password module with client presets, Effect, Encore, Convex and Lynx docs, hosted database opt-in tests, and the react context split. Apple App Attest assertions and receipts, TPM credential activation and certify, attestation constraints in authorize and signed bundles. Audit repository and an internal Kysely kernel verified on SQLite, Postgres, MySQL and MSSQL. One Tap, magic link and email OTP no longer log in to or claim an unverified account without clearing its credentials. Audit text is scrubbed of NUL before hashing, MySQL increment reads back the row it updated, and the lockfile is refreshed.
…ult to the mock provider
| const cols = (pk[2] as string).split(",").map((c) => c.trim()); | ||
| let bytes = 0; | ||
| for (const col of cols) { | ||
| const m = new RegExp(`${col.replace(/[[\]]/g, "\\$&")} (NVARCHAR\\((\\d+)\\)|BIGINT)`).exec( |
Applies the pending changesets with changeset version. Core lands on 2.0.0. Versions below are the previous version on this branch (core, mcp, gateway and create-theauth-app were already ahead of npm) and the new one. - @glinr/create-theauth-app: 0.5.0 to 0.6.0 - @glinr/theauth: 1.3.0 to 2.0.0 - @glinr/theauth-astro: 5.1.0 to 6.0.0 - @glinr/theauth-cli: 1.2.0 to 1.3.0 - @glinr/theauth-client: 1.1.1 to 1.2.0 - @glinr/theauth-conformance: 0.2.0 to 0.3.0 - @glinr/theauth-dashboard: 0.3.0 to 0.4.0 - @glinr/theauth-electron: 0.3.0 to 0.4.0 - @glinr/theauth-email: 4.1.0 to 4.1.1 - @glinr/theauth-expo: 0.4.0 to 0.5.0 - @glinr/theauth-express: 5.1.0 to 6.0.0 - @glinr/theauth-fastify: 5.1.0 to 6.0.0 - @glinr/theauth-gateway: 4.1.0 to 4.2.0 - @glinr/theauth-hono: 5.1.0 to 6.0.0 - @glinr/theauth-mcp: 1.2.0 to 2.0.0 - @glinr/theauth-nestjs: 5.1.0 to 6.0.0 - @glinr/theauth-nextjs: 5.1.0 to 6.0.0 - @glinr/theauth-nextjs-auth: 0.2.0 to 1.0.0 - @glinr/theauth-nuxt: 5.1.0 to 6.0.0 - @glinr/theauth-plugin-discovery: 4.0.1 to 4.0.2 - @glinr/theauth-plugin-telemetry: 4.0.0 to 4.1.0 - @glinr/theauth-prisma: 4.0.0 to 4.1.0 - @glinr/theauth-react: 0.7.1 to 0.8.0 - @glinr/theauth-solidstart: 5.1.0 to 6.0.0 - @glinr/theauth-svelte: 0.3.0 to 0.4.0 - @glinr/theauth-sveltekit: 5.1.0 to 6.0.0 - @glinr/theauth-tanstack: 5.1.0 to 6.0.0 - @glinr/theauth-test-utils: 0.2.0 to 0.3.0 - @glinr/theauth-ui: 0.3.0 to 0.4.0 - @glinr/theauth-vue: 0.3.0 to 0.4.0 - theauth: 0.2.6 to 0.2.7 - @glinr/theauth-example-admin-approval-flow: 0.0.2 to 0.0.3 (private) - @glinr/theauth-example-agent-accountability: 0.0.2 to 0.0.3 (private) - @glinr/theauth-example-basic-agent: 0.0.15 to 0.0.16 (private) - @glinr/theauth-example-migrate-from-auth0: 0.0.10 to 0.0.11 (private) - @glinr/theauth-example-migrate-from-better-auth-agent-plugin: 0.0.10 to 0.0.11 (private) - marquee-demo: 0.1.12 to 0.1.13 (private) - playground: 0.1.1 to 0.1.2 (private) Also updates the README package table and the version fields in packages/mcp/server.json.
Pin @vitest/coverage-v8 to 4.1.11 to match the installed vitest 4.1.11. The caret range resolved 4.1.12 and left an unmet peer warning. Lockfile refreshed. The other unlisted dependencies from the knip run were already declared (@tanstack/react-query in react and ui-headless, h3 in nuxt, vitest in test-utils, @glinr/theauth in telemetry). server-only in nextjs-auth, the dashboard package in cli and tailwindcss in dashboard were left alone.
…oke freshness test
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.




This is the second agent auth release branch. It has 41 commits on top of main, adds devices and robots next to agents, and fixes a batch of bugs that review and real database runs turned up. Nothing here is released or tagged. The later commits add quorum approvals, bring your own Drizzle, three more secondary storage backends, Supabase and WorkOS importers, client binding fixes, single-use repositories, resumable GDPR erasure, SPIFFE workload identity routes, hot path indexes and a libSQL provider. See What is new and Breaking changes.
Read this before merging
Merging this to main will publish packages.
publish.ymlpublishes any package whose version differs from npm once main CI is green, and main already carries @glinr/theauth 1.3.0, mcp 1.2.0 and gateway 4.1.0, none of which are on npm. This branch also fixes the lockfile that is keeping main red. So the first green run ships whatever version main holds. There are 106 pending changesets, 23 of them majors for core, so the plan is 2.0.0. Runpnpm changeset versionon this branch and commit it in the same merge. The full order and the patch release table are in docs-local/tasks/release-plan-2.0.md (local file, not in the repo).Security fixes in versions already on npm
These affect @glinr/theauth 1.2.0 and need a 1.2.x patch release and advisories. The drafts for the first four exist and have not been sent.
tool:dbdelegatetool:db:secrets, which the parent could not use itself.maxCallsPerHour, tabletheauth_rate_limits) read its counters and then wrote one back, so parallel calls could all see room. The changeset reports 24 parallel calls against a limit of 10 all getting through. The fix reserves a slot with one guarded increment and hands it back when a call is over the limit, so the budget is never exceeded. It is a patch change. A call can be refused when it would have just fit.Fixed before any release: org invitations could be accepted by any signed in user, and an org admin could promote themselves to owner.
DELETE /delegations/:idon the admin API never wrote the shared revocation denylist, so MCP tokens tied to a revoked delegation stayed valid on other instances. Approval hashes treatedNaNandInfinitylikenull.What is new
theauth device, REST routes and a dashboard Devices page.devices.revocationsset,issueBundleandGET /device/bundlereturn a signed list of the owner's revoked and suspended device ids next to the policy bundle.verifyDeviceBundlerefuses a bundle withrevokedwhen the device is listed, and withstale_revocationswhen the snapshot is missing, expired or older thanmaxAgeSeconds.verifyRevocationSnapshotanswers membership for peers. The Go packageiot/deviceverifies the same format against shared vectors.createAndroidKeyAttestationVerifierandgoogleRevocationList, plustheauth device attest android. Tested against the real chains in google/android-key-attestation.createAppleAppAttestVerifierandtheauth device attest apple. Tested against Apple's own sample attestation and two real device recordings.createTpm2QuoteVerifierandtheauth device attest tpm. Tested against real Google Compute Engine vTPM quotes. This is not physical TPM hardware.theauth ros2 exportwrites SROS2 and DDS Security permissions files. The XML validates against the sros2 schemas, and a gate (guardRobotCommand) fails closed and never waits on the network for a safety stop. Opt in path aware matching (resourceMatching: "path") soros2:/fleet/*means what it says.acr,amrandauth_time, a passkey step up on the current session, and DPoP or mTLS bound agent tokens.approval.pendingwebhook. Input provenance (keyed hashes of what was in the agent's context) in the audit chain, rotating pseudonyms, and a verifier for the Go head checkpoint that finally lets TypeScript see tail truncation of a Go chain.approval.request({ requiredApprovals: 2 })requires N distinct approvers, from 1 to 10 (default 1). Approvers must differ from the requester and from each other, one denial ends the request, and each approval writes its own audit row. Argument binding, single spend and expiry apply to the whole quorum. The admin API takesrequiredApprovals, the dashboard inbox shows "1 of 2 approvals", and theapproval.pendingpayload addsrequiredApprovalsandapprovalsReceived. A newapproval.voteevent reports progress. Migration 0013 adds therequired_approvalscolumn and thetheauth_approval_votestable.databaseHookstakesbeforeandafterhooks for create and update onuser,sessionandaccount. Abeforehook can returnfalseto abort or{ data }to replace allowed fields. It fails closed on a throw, a timeout or a malformed result (DatabaseHookError).afterhooks run once the row is committed and cannot break the write. Hooks run outside every single use claim statement, so magic links, OTP codes, OAuth state and refresh rotation stay atomic.database: { drizzle, dialect }takes an existing Drizzle instance for"sqlite","postgres"or"mysql"instead of a URL. It uses the sameDatabaseOpenerseam, migrations use the dialect, andskipMigrationsworks as before.drizzle-kernel.tswraps conditional claims so each claim is decided by the row count of its conditional statement.StoreConflictErrorreads driver codes for the nine unique index conflicts and keeps the text match as a fallback.@glinr/theauth-test-utilsadds a./store-contractsubpath withrunStoreContract, which covers claim races with 24 parallel callers, unique conflicts and audit chain order for any backend. Commit f7238b3 runs the store contract suite in the database matrix.mongoStorage(collection)for MongoDB 4.4+,sqlStorage({ dialect, run })for any SQL database including Microsoft SQL Server, andkyselyStorage(db, { dialect, CompiledQuery })for any Kysely dialect. None adds a dependency, since you pass your own client. All keep exact per key counters and follow the sameSecondaryStoragecontract as the memory, database and Redis backends. Also fixesredisStorage().incr(), which threw on a key that held plain text.supabaseandworkossources for the user importer andtheauth migrate. Supabase readsauth.usersrows (JSON or CSV, with optionalauth.identities) and keeps bcrypt hashes for lazy migration. WorkOS reads List users API output and accepts an optional hash file from support, because the API returns no hashes. Guides atmigrate/from-supabaseandmigrate/from-workos.vonageOtpSenderandplivoOtpSender, next totwilioOtpSender.hydrateSession(initial)oncreateSessionStore. The React provider, Vue plugin, Svelte client and the new Solid helper use it.createSolidAuthwraps the store as Solid accessors without depending on solid-js. Refetch runs only while something is subscribed, shares one request across overlapping refreshes, and can be stopped.GET /auth/session, and sign out callsPOST /auth/sign-outso the session is revoked on the server.theauth.integrations, tested against the real frameworks. The old recipe in the docs used a package that is not a dependency and had never run; a test now runs every Python block on that page.ROS_SECURITY_STRATEGY=Enforceon ROS 2 Humble (Fast DDS 2.6.12, sros2 0.10.9), an allowed topic flows, a topic outside the policy is refused for publisher and subscriber, and a gated command topic is refused for a normal enclave. That run found that an exported policy alone could not start any node (its own rosout and parameter endpoints were denied), so the CLI now adds a node baseline by default. The docs say exactly what was and was not run.theauth doctor --security, framework helpers for the Vercel AI SDK and OpenAI Agents SDK, a generated OpenAPI contract with drift tests, and Go and Python clients that match it.gdpr.deleteUser. It runs as ordered steps with a progress marker, so a crash or database error halfway is finished by calling it again, and two concurrent calls leave one clean result. Audit erasure writes eachaudit.erasurerecord once.gdpr.anonymizeUserapplies its credential deletes and user update as one atomic batch, andorganization.removeis one atomic batch. Deleting a user also revokes the delegation links their agents give or receive, cascading downstream, and keeps the revoked rows so downstream grants stop working.requiredApprovalsonTheAuthApprovalRequest, aTheAuthApprovalVotemodel, andcastApprovalVoteandlistApprovalVotes, with the same single vote guarantee as core. A newtheauth approvalscommand (list, show, approve, deny, request with--required-approvals) drives the admin API. The Python and Go clients send and read the quorum fields. A fix stops the completing call from reporting every counted approver when concurrent votes pushed the count past the quorum.devices.spiffeset, the handler servesPOST /device/spiffe/verify,POST /devices/spiffe/verify,POST /devices/spiffe/tokenandPOST /devices/:id/spiffe. Minting and linking are admin only. An X.509 chain is never read from the body of the device route, only from yourclientCertificatecallback or the one header you name intrustedProxyHeader(off by default). Every route is rate limited and fails closed. Every verification outcome is written to the audit log asdevice.spiffe.authenticateordevice.spiffe.verifywhen it maps to a device or agent, with the SPIFFE ID, failure code and certificate thumbprint, never the certificate or token. The dashboard Devices page lists SPIFFE identities and can link one.@glinr/theauth/testing/devices.createDeviceFleet({ size })enrolls simulated devices against a real in-memory theAuth through the real HTTP handler, with a virtual clock.runDeviceConformance(fleet)checks enrollment, assertions, bundle and revocation snapshot verification, offline and online use, clock skew, rollback and revocation during a session. Measured p50 and p99 at 1000 devices are indocs/device-conformance-kit.mdx.0014-hot-path-indexes. It adds indexes for agent lookup by token hash, active agents per owner, permissions per agent, delegation links in both directions, the hourly rate limit read and the pending approval list. Without them these lookups scanned their table on SQLite and Postgres. Along with reused crypto keys, cheaper canonicalization and matching, and fewer queries on authorize and admin lists, the hot paths are faster with the same decisions. Each fast path is pinned to the code it replaced by tests. The numbers below come from one laptop, an Apple M4 Max under load, using SQLite through better-sqlite3, so they show direction and rough size, not a figure for your deployment. Taken fromdocs/performance.mdx, with a SQLite file of 50,000 agents, 200,000 permissions and 50,000 delegation links:authorizewith a direct permission went from 4.10 ms to 0.077 ms,authorizethrough a three hop delegation chain from 9.88 ms to 0.36 ms, the token hash lookup from 5.33 ms to 0.12 ms, and the admin pending approvals list (100 quorum requests) from 2.29 ms to 0.51 ms. Canonical JSON for the approval hash on a 20 row payload went from 38.6 us to 10.9 us. One result got slightly worse: an offline device bundle decision with two permissions is about 7 percent slower, because the new colon matcher costs a little more on very short inputs. The DPoP proof path did not change, since its key is new on every request.createCloudflareProvider(Sign in with Cloudflare, profile from the Cloudflare API/user, basic or post client auth) andcreatePaybinProvider(Paybin OpenID Connect with PKCE), both in@glinr/theauth/auth, with provider pages.database: { provider: "libsql", url, authToken? }for Turso and other libSQL servers, or afile:URL, backed by the optional@libsql/clientpeer. It migrates with the SQLite schema. A Turso, Neon and Supabase page covers connection modes and SSL.advanced.database.generateIdpicks how user, session and OAuth account ids are made."uuid"(default),"uuidv7"(time ordered, RFC 9562) or a function. Results are validated (1 to 191 characters). Database generated ids throwTHEAUTH_ID_UNSUPPORTED, because every table stores and references ids as text. ExportsgenerateUuidV7andIdGenerationError.Breaking changes
Each has a changeset and a section in docs/upgrading-to-2.mdx. The ones most likely to hurt: self service API keys can only carry permissions the user holds (capped by the creator's org roles when the org module is on, and an empty list otherwise; the org check is an exact match, so a role entry like
agents:*does not coveragents:read), the adminDELETE /delegations/:idnow answers 204 like the core route instead of 200 with a body, org and apiKeys handlers need an authenticate resolver, approval hashes change for arguments with<,>,&, U+2028, U+2029 or astral keys, dashboard audit results usehasMoreinstead oftotal, and adapter peer ranges now allow core below 3.0.0.Added in waves 12 and 13:
SessionStore.refresh()now returns a promise, andSessionStore.signOut()returnsPromise<StoreActionResult>. Code that treats either as synchronous must await it./auth/username/sign-inand/auth/username/sign-up, and the first argument is sent asusername. Before, they called/auth/sign-inand/auth/sign-up, which the core never served. If you served your own/auth/sign-inor/auth/sign-upfor these bindings, setsignInPath,signUpPathandidentifierField(React: themanagedprop; Vue and Svelte: the plugin or client options). Expo and Electron take the same three options, and apps that relied on/sign-in/emailand/sign-up/emailmust set them.attestation.requireNonceis false. The enrollment hook input now carriescredentialKindandcredentialKeyThumbprint.TwoFactorVerifyposts{ code }to/auth/2fa/verify, and a backup code goes in the same field.OAuthButtonsnavigates to/auth/oauth/authorize/:provider, and the server builds the callback URL.DELETE /agents/:id.timeoutMs), and their errors carry only the HTTP status and vendor error code, not the response body.PUTnow removes only the members missing from the list and adds only the new ones. Before, it deleted and re-inserted every member, which dropped roles other thanmember.Also in this refresh:
createTheAuthapplies it, or runtheauth db up.UserBannedErrorand writes a deniedsign_inaudit row. This covers password, username, magic link, passkey, OAuth, SAML and SSO sign-ins.[deleted-are reserved.user.createrejects them.hmacKey, else the stored audit secret). An id computed the old way, an unsalted SHA-256 prefix, no longer matches.What I ran
Typecheck for core, cli, dashboard, mcp, gateway and the adapters. Every new or changed test file plus the related areas (about 200 core files), every adapter, the dashboard and cli suites. The dialect suites ran for real against MySQL 8.4.11 and Postgres 14, including parallel single use claims. The pre push hook ran the whole core suite. New tests were run six times in parallel to catch flakes.
For the commits since the last refresh (746da30 to 0231034), the commit messages, changesets and docs record these runs and nothing more. The hot path numbers in
docs/performance.mdxcome from the micro benchmarks andbenchmarks/scale.mjson SQLite, one laptop, with the load average near 14. The libSQL changeset says the store contract suite passes on a real libSQL file, both through the provider and through a bring your owndrizzle-orm/libsqlhandle. The Nitro and Waku recipes run against the real frameworks. The migration test asserts the query plans before and after 0014. The equivalence tests named indocs/performance.mdxcompare each fast path with the code it replaced. The commit messages for 0231034 and 1ec0e34 record a test change and generated file fixes, with no run results.For the wave 12 and 13 commits, the commit messages and changesets record no runs beyond the fixture tests named under device attestation: the Android chains, Apple's sample and two device recordings, and the Google Compute Engine vTPM quotes. The store contract suite is wired into the database matrix (f7238b3), and the commit messages do not record its results.
Not verified
Postgres 16 and 17, MariaDB and older MySQL versions. ROS 2 distros other than Humble, other DDS middleware, rclcpp nodes, certificate expiry and the runtime gate node. The Python snippets in the integration docs and the rclcpp sketch. TinyGo builds of the Go device client beyond the four smoke targets in the Go PR. A Zenoh exporter does not exist yet. SAML is still hand written code with no external audit, so I would call it experimental. The robotics pages make no safety or compliance claims.
Also not verified:
docs/performance.mdxare SQLite only.Versions this merge publishes
The branch already carries the version bump (
changeset version, two commits at the end). When main CI is green,publish.ymlpublishes 29 packages whose version differs from npm:@glinr/theauth1.3.0 to 2.0.0 (npm has 1.2.0) and@glinr/theauth-mcp1.2.0 to 2.0.0 (npm has 1.1.1).@glinr/theauth-nextjs-auth0.2.0 to 1.0.0.theauthalias 0.2.7. Publish those by hand if wanted. The MCP Registry entry (packages/mcp/server.json, 2.0.0) must be published after the npm publish of mcp 2.0.0.Core 1.2.1 (security patch for 1.x) is a separate branch,
release/patch-1.2.1-2026-10-19, not part of this PR.Needs a decision
The 2.0 version bump and whether to cut a 1.2.1 patch first.
Whether the client route default change (
/auth/username/sign-in) is documented as breaking for 2.0 only, or also shipped as a 1.2.x patch.