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
33 changes: 33 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# AGENTS.md

## Purpose

`@corbits/agent-token` authenticates deployed agents calling back into their
Interchange hub. It owns the `agent_token` Postgres schema, the mint, list and
revoke routes, and the middleware that verifies a bearer. It does not own
grants, tenants or outbound credentials an agent uses to call other services.

## Layout

- `src/tokens.ts` — `mintAgentToken`, `revokeAgentToken`, `verifyAgentToken` and the SHA-256 digest.
- `src/mount.ts` — `mountAgentTokens`, the tenant-scoped mint, list and revoke routes.
- `src/middleware.ts` — `requireAgentToken` and `createAgentTokenVerifier`.
- `src/workflow-run-scope.ts` — the run-scope types shared by run-scoped mounts.
- `src/schema.ts` — the `agent_token` schema's one table.
- `src/migrations.ts` — `runAgentTokenMigrations`, applies `migrations/*.sql`.
- `src/index.ts` — the only module consumers import from (plus `./migrations`).
- `e2e/` — real-Postgres suites, including the 0.1.0 upgrade test.

## Rules

- Store only `sha256(token)`; the plaintext appears once, in the mint response.
- A token is valid only on routes of the tenant that minted it; anything else is a 401.
- The acting tenant comes from the host context, never from a path parameter.
- Every route runs the host's `requireGrant`.
- Every migration statement is idempotent; there is no ledger.

## Local development

```sh
bun install && bun run check
```
29 changes: 18 additions & 11 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,15 @@
## Development

```sh
git clone https://github.com/corbitsdev/corbits-agent-token.git
cd corbits-agent-token
bun install
createdb agent_token_dev
export DATABASE_URL=postgres://localhost:5432/agent_token_dev

bun run check # oxlint, oxfmt --check, typecheck, tests (what CI runs)
bun run format # oxfmt
bun run build
bun run check
```

The suites in `e2e/` run against a real Postgres and skip when `DATABASE_URL` is unset. CI sets it.
`bun run check` runs typecheck, lint, format check and unit tests. `bun run format` rewrites the tree.

Contributors sign the [CLA](CLA.md) on their first PR; the CLA bot explains how.

- `e2e/agent-token.drizzle.test.ts` runs Interchange's migrations into a scratch schema and exercises the routes, the middleware and the verifier.
- `e2e/upgrade-from-0.1.0.test.ts` creates its own database, builds it with the published 0.1.0 (the `agent-token-0.1.0` dev dependency), then upgrades it with this build.
`bun run test:e2e` needs Postgres in `DATABASE_URL`, e.g. `DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres bun run test:e2e`; the suites skip when it is unset. `e2e/agent-token.drizzle.test.ts` exercises the routes, middleware and verifier on a scratch schema. `e2e/upgrade-from-0.1.0.test.ts` builds its own database with the published 0.1.0 (the `agent-token-0.1.0` dev dependency), then upgrades it with this build.

## Migrations

Expand All @@ -29,3 +23,16 @@ Commit subjects and PR titles follow [Conventional Commits](https://www.conventi
Add `!` only for public API breaks: removed or renamed exports, changed signatures, newly required params. Peer and dependency range changes are `build(deps):` with no `!`.
Keep subjects imperative, lowercase after the colon, 72 characters or less, and free of ticket IDs.
Every PR links its issue with a `Closes <issue id>` line in the PR body.

## Releasing

Releases are manual. On a clean, up-to-date `main`:

```sh
npm version <patch|minor> -m "chore(release): %s"
git push --follow-tags
gh release create "v$(node -p 'require("./package.json").version')" --generate-notes
npm publish
```

Bump minor only for breaking API changes; everything else is a patch. `prepack` builds `dist/` from the tagged commit.
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# @corbits/agent-token

[![npm](https://img.shields.io/npm/v/@corbits/agent-token.svg)](https://www.npmjs.com/package/@corbits/agent-token) [![License: LGPL-2.1](https://img.shields.io/badge/license-LGPL--2.1-green.svg)](https://github.com/corbitsdev/corbits-agent-token/blob/main/LICENSE)

Bearer tokens that a deployed agent presents when it calls back into its Interchange hub (the multi-tenant control plane): mint, list and revoke routes, SHA-256 digests in Postgres, and a Hono middleware that verifies the bearer against the route's tenant. A Corbits hub module that mounts on `@intx/hub-api` and `@intx/db`.

## Why @corbits/agent-token?
Expand Down Expand Up @@ -61,11 +59,13 @@ Mounts relative routes on a `Hono<TenantEnv>`. Put it under the hub's tenant pre
| `requireGrant` | `MiddlewareHandler` | The grant check for minting a credential, run on every route. |
| `resolveDefinition` | `ResolveDefinition` | `(tenantId, definitionId) => boolean \| Promise<boolean>`: whether the tenant owns it. |

| Route | Result |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /agent-tokens` | `200 { tokens }`, without plaintext or digest. |
| `POST /agent-tokens` | Mints for `{ definitionId, name }`: `201 { token }` with the plaintext. `400 invalid_body` on a bad body, `404 not_found` for a definition the tenant does not own. |
| `DELETE /agent-tokens/:id` | Revokes: `200 { ok: true }`, or `404 not_found`. Revocation is final. |
| Route | Result |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GET /agent-tokens` | `200 { tokens }`, without plaintext or digest. |
| `POST /agent-tokens` | Mints for `{ definitionId, name }`: `201 { token }` with the plaintext. `400 invalid_body` on a bad body or a field over 256 characters, `404 not_found` for a definition the tenant does not own. |
| `DELETE /agent-tokens/:id` | Revokes: `200 { ok: true }`, or `404 not_found`. Revocation is final. |

Tokens do not expire: each stays valid until it is revoked.

### `requireAgentToken({ db })`

Expand Down
30 changes: 30 additions & 0 deletions e2e/agent-token.drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,36 @@ describeIfDb("agent tokens", () => {
}
});

test("the mount refuses a name or definitionId over 256 characters", async () => {
const { db, close } = createDB({ ...target, schema: SCHEMA });
try {
const tenantId = `tnt_len_${randomUUID().slice(0, 8)}`;
await seedTenant(db, tenantId);
const app = mountedApp(db, tenantId, { owns: ["x".repeat(257)] });
const mint = (body: unknown) =>
app.request("/agent-tokens", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});

expect(
(await mint({ definitionId: "def_artifacts", name: "n".repeat(257) }))
.status,
).toBe(400);
expect(
(await mint({ definitionId: "x".repeat(257), name: "artifacts" }))
.status,
).toBe(400);
expect(
(await mint({ definitionId: "def_artifacts", name: "n".repeat(256) }))
.status,
).toBe(404);
} finally {
await close();
}
});

test("the middleware admits a live bearer on its own tenant and 401s everything else", async () => {
const { db, close } = createDB({ ...target, schema: SCHEMA });
try {
Expand Down
4 changes: 2 additions & 2 deletions src/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export type MountAgentTokensOpts<
};

const MintBody = type({
definitionId: "string > 0",
name: "string > 0",
definitionId: "0 < string <= 256",
name: "0 < string <= 256",
});

/** Mount `/agent-tokens` onto the host's app, under its tenant prefix. */
Expand Down
Loading