From 19175a613c255f162055930c24bfe3fc2e67d096 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Thu, 24 Sep 2026 23:22:41 -0700 Subject: [PATCH 1/2] test: move real-Postgres suites to e2e/ and drop helper unit tests Bearer parsing is covered through the public verifier instead. --- {src => e2e}/agent-token.drizzle.test.ts | 17 ++++++++++++----- package.json | 2 +- src/tokens.test.ts | 22 ---------------------- tsconfig.build.json | 3 +-- tsconfig.json | 2 +- 5 files changed, 15 insertions(+), 31 deletions(-) rename {src => e2e}/agent-token.drizzle.test.ts (94%) delete mode 100644 src/tokens.test.ts diff --git a/src/agent-token.drizzle.test.ts b/e2e/agent-token.drizzle.test.ts similarity index 94% rename from src/agent-token.drizzle.test.ts rename to e2e/agent-token.drizzle.test.ts index 5f7ed2f..c2a10a9 100644 --- a/src/agent-token.drizzle.test.ts +++ b/e2e/agent-token.drizzle.test.ts @@ -9,11 +9,11 @@ import type { TenantEnv } from "@intx/hub-api"; import { eq } from "drizzle-orm"; import { Hono, type MiddlewareHandler } from "hono"; -import { runAgentTokenMigrations } from "./migrations"; -import { hashAgentToken, mintAgentToken, revokeAgentToken, verifyAgentToken } from "./tokens"; -import { mountAgentTokens } from "./mount"; -import { createAgentTokenVerifier, requireAgentToken } from "./middleware"; -import type { WorkflowRunScopeEnv } from "./workflow-run-scope"; +import { runAgentTokenMigrations } from "../src/migrations"; +import { hashAgentToken, mintAgentToken, revokeAgentToken, verifyAgentToken } from "../src/tokens"; +import { mountAgentTokens } from "../src/mount"; +import { createAgentTokenVerifier, requireAgentToken } from "../src/middleware"; +import type { WorkflowRunScopeEnv } from "../src/workflow-run-scope"; const databaseUrl = process.env.DATABASE_URL; const describeIfDb = databaseUrl === undefined ? describe.skip : describe; @@ -260,6 +260,13 @@ describeIfDb("agent tokens", () => { expect(await identityFor(`Bearer ${minted.token}`, otherTenantId)).toBeNull(); expect(await identityFor()).toBeNull(); expect(await identityFor("Bearer nope")).toBeNull(); + expect(await identityFor(` bearer ${minted.token} `)).toEqual({ + id: minted.id, + tenantId, + definitionId: "def_artifacts", + }); + expect(await identityFor(`Basic ${minted.token}`)).toBeNull(); + expect(await identityFor("Bearer")).toBeNull(); await revokeAgentToken(db, { tenantId, id: minted.id }); expect(await identityFor(`Bearer ${minted.token}`)).toBeNull(); } finally { diff --git a/package.json b/package.json index bfe81a0..ff5cfd2 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "build": "tsc -p tsconfig.build.json", "prepack": "bun run build", "typecheck": "tsc --noEmit", - "test": "bun test ./src" + "test:e2e": "bun test ./e2e" }, "dependencies": { "arktype": "^2.2.0" diff --git a/src/tokens.test.ts b/src/tokens.test.ts deleted file mode 100644 index 1ecc251..0000000 --- a/src/tokens.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, test } from "bun:test"; - -import { agentTokenHashEquals, bearerFromAuthorization, hashAgentToken } from "./tokens"; - -describe("bearerFromAuthorization", () => { - test("reads a bearer, case-insensitively, and ignores anything else", () => { - expect(bearerFromAuthorization("Bearer abc123")).toBe("abc123"); - expect(bearerFromAuthorization(" bearer abc123 ")).toBe("abc123"); - expect(bearerFromAuthorization("Basic abc123")).toBeUndefined(); - expect(bearerFromAuthorization("Bearer")).toBeUndefined(); - expect(bearerFromAuthorization(undefined)).toBeUndefined(); - }); -}); - -describe("agentTokenHashEquals", () => { - test("compares digests without throwing on a length mismatch", () => { - const digest = hashAgentToken("secret"); - expect(agentTokenHashEquals(digest, hashAgentToken("secret"))).toBe(true); - expect(agentTokenHashEquals(digest, hashAgentToken("other"))).toBe(false); - expect(agentTokenHashEquals(digest, "short")).toBe(false); - }); -}); diff --git a/tsconfig.build.json b/tsconfig.build.json index 28cb440..861b363 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -12,6 +12,5 @@ "outDir": "dist", "rootDir": "src" }, - "include": ["src"], - "exclude": ["src/**/*.test.ts"] + "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index 0a0b189..1977cfa 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,5 @@ "allowImportingTsExtensions": true, "types": ["bun"] }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "e2e/**/*.ts"] } From 6b33f494738cce9f243c018e246dfc20ca76ba65 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 25 Sep 2026 11:08:26 -0700 Subject: [PATCH 2/2] test: prove 0.1.0 databases and tokens survive the upgrade Builds a database with the published 0.1.0, then upgrades it in place: live tokens still verify and pass requireAgentToken, revoked ones stay revoked, and the migration re-runs cleanly. --- bun.lock | 3 + e2e/upgrade-from-0.1.0.test.ts | 120 +++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 124 insertions(+) create mode 100644 e2e/upgrade-from-0.1.0.test.ts diff --git a/bun.lock b/bun.lock index 0b850a2..714252a 100644 --- a/bun.lock +++ b/bun.lock @@ -11,6 +11,7 @@ "@intx/db": "0.4.0", "@intx/hub-api": "0.4.0", "@types/bun": "1.3.9", + "agent-token-0.1.0": "npm:@corbits/agent-token@0.1.0", "drizzle-orm": "0.45.2", "hono": "4.13.8", "postgres": "3.4.9", @@ -122,6 +123,8 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "agent-token-0.1.0": ["@corbits/agent-token@0.1.0", "", { "dependencies": { "@intx/db": "^0.4.0", "arktype": "^2.2.0", "drizzle-orm": "^0.45.2", "hono": "^4.11.9", "postgres": "^3.4.8" } }, "sha512-ESuG8gV17dqhZwsG2Tj7P8KtldF+Es7PLopJHW6w3G3LaOy0lpC7SpBAJuNZR0Hlvxi2LOueR3UK35JCZslX8A=="], + "arkregex": ["arkregex@0.0.8", "", { "dependencies": { "@ark/util": "0.56.2" } }, "sha512-PJcx6G1kQTgLKPUbeYlYecDRaKq15AMSGVajlKFYWlPeJRQL+j3dKE6tyMs40HZ99djS1l9Vhl3ezAHy9JBIqQ=="], "arktype": ["arktype@2.2.3", "", { "dependencies": { "@ark/schema": "0.56.2", "@ark/util": "0.56.2", "arkregex": "0.0.8" } }, "sha512-7W+0RLTUNJiBFIIZXwOQxSR8Z273IAd6IvqBeG9+gHnQKFsIx2C0iOtGTmMrPnlX4qLXyc5+ll7A0BIj9WrbTg=="], diff --git a/e2e/upgrade-from-0.1.0.test.ts b/e2e/upgrade-from-0.1.0.test.ts new file mode 100644 index 0000000..4a7c92c --- /dev/null +++ b/e2e/upgrade-from-0.1.0.test.ts @@ -0,0 +1,120 @@ +// DB-gated: skipped when no DATABASE_URL is reachable. Builds a database +// with the published 0.1.0 package in a scratch database of its own, then +// upgrades it in place with this build: tokens minted by 0.1.0 keep working. +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { randomUUID } from "node:crypto"; +import { createDB, runMigrations, schema } from "@intx/db"; +import type { TenantEnv } from "@intx/hub-api"; +import * as published from "agent-token-0.1.0"; +import { sql } from "drizzle-orm"; +import { Hono } from "hono"; +import postgres from "postgres"; + +import { runAgentTokenMigrations } from "../src/migrations"; +import { requireAgentToken } from "../src/middleware"; +import { verifyAgentToken } from "../src/tokens"; + +const databaseUrl = process.env.DATABASE_URL; +const describeIfDb = databaseUrl === undefined ? describe.skip : describe; + +const DATABASE = `agent_token_upgrade_${randomUUID().slice(0, 8)}`; +const TENANT_ID = "tnt_upgrade"; + +describeIfDb("upgrading a 0.1.0 database", () => { + const adminUrl = databaseUrl ?? "postgres://localhost:5432/unused"; + const url = new URL(adminUrl); + url.pathname = `/${DATABASE}`; + const target = { + host: url.hostname, + port: url.port ? Number(url.port) : 5432, + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + database: DATABASE, + }; + const admin = postgres(adminUrl, { max: 1, onnotice: () => undefined }); + const live = { token: "", id: "" }; + const revoked = { token: "" }; + + beforeAll(async () => { + await admin.unsafe(`CREATE DATABASE "${DATABASE}"`); + await runMigrations(target, { schema: "public" }); + await published.applyAgentTokenMigrations(url.toString()); + + const { db, close } = createDB({ ...target, schema: "public" }); + try { + await db.insert(schema.tenant).values({ + id: TENANT_ID, + name: TENANT_ID, + slug: "tnt-upgrade", + domain: "tnt-upgrade.workbench.test", + }); + const minted = await published.mintAgentToken(db, { + tenantId: TENANT_ID, + definitionId: "def_artifacts", + name: "artifacts", + }); + live.token = minted.token; + live.id = minted.id; + const gone = await published.mintAgentToken(db, { + tenantId: TENANT_ID, + definitionId: "def_artifacts", + name: "gone", + }); + revoked.token = gone.token; + await published.revokeAgentToken(db, { + tenantId: TENANT_ID, + id: gone.id, + }); + } finally { + await close(); + } + + await runAgentTokenMigrations(target, { schema: "public" }); + await runAgentTokenMigrations(target, { schema: "public" }); + }); + + afterAll(async () => { + await admin.unsafe(`DROP DATABASE IF EXISTS "${DATABASE}" WITH (FORCE)`); + await admin.end({ timeout: 5 }); + }); + + test("tokens minted by 0.1.0 still verify, and revoked ones stay revoked", async () => { + const { db, close } = createDB({ ...target, schema: "public" }); + try { + expect(await verifyAgentToken(db, live.token)).toEqual({ + id: live.id, + tenantId: TENANT_ID, + definitionId: "def_artifacts", + }); + expect(await verifyAgentToken(db, revoked.token)).toBeUndefined(); + const foreignKeys = await db.execute(sql` + SELECT confrelid::regclass::text AS target FROM pg_constraint + WHERE conrelid = 'agent_token.token'::regclass AND contype = 'f' + `); + expect([...foreignKeys]).toEqual([{ target: "tenant" }]); + + const app = new Hono(); + app.use(async (c, next) => { + const [tenant] = await db.select().from(schema.tenant); + if (tenant === undefined) return c.json({ error: "not_found" }, 404); + c.set("tenant", tenant); + await next(); + return undefined; + }); + app.get("/guarded", requireAgentToken({ db }), (c) => + c.json({ id: c.get("agentToken").id }), + ); + const ok = await app.request("/guarded", { + headers: { authorization: `Bearer ${live.token}` }, + }); + expect(ok.status).toBe(200); + expect(await ok.json()).toEqual({ id: live.id }); + const gone = await app.request("/guarded", { + headers: { authorization: `Bearer ${revoked.token}` }, + }); + expect(gone.status).toBe(401); + } finally { + await close(); + } + }); +}); diff --git a/package.json b/package.json index ff5cfd2..822fc11 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@intx/db": "0.4.0", "@intx/hub-api": "0.4.0", "@types/bun": "1.3.9", + "agent-token-0.1.0": "npm:@corbits/agent-token@0.1.0", "drizzle-orm": "0.45.2", "hono": "4.13.8", "postgres": "3.4.9",