Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
120 changes: 120 additions & 0 deletions e2e/upgrade-from-0.1.0.test.ts
Original file line number Diff line number Diff line change
@@ -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<TenantEnv>();
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();
}
});
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand Down
22 changes: 0 additions & 22 deletions src/tokens.test.ts

This file was deleted.

3 changes: 1 addition & 2 deletions tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,5 @@
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["src/**/*.test.ts"]
"include": ["src"]
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
"allowImportingTsExtensions": true,
"types": ["bun"]
},
"include": ["src/**/*.ts"]
"include": ["src/**/*.ts", "e2e/**/*.ts"]
}