Skip to content
Open
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 examples/frameworks/hono/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
APORT_API_KEY=your_api_key_here
APORT_BASE_URL=https://api.aport.io
PORT=3000
114 changes: 114 additions & 0 deletions examples/frameworks/hono/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# APort Hono Framework Example

Runnable APort policy verification for [Hono](https://hono.dev/) applications. The example protects refund and admin routes while keeping the public route open.

## What This Shows

- Hono middleware that verifies an agent before a route handler runs
- Agent ID extraction from headers, query parameters, or JSON body
- Request context passed into the policy verification call
- Fail-closed strict mode with an optional non-strict mode for demos
- Unit tests that run without real APort credentials by injecting a mock client

## Quick Start

```bash
cd examples/frameworks/hono
npm install
npm test
npm start
```

The server starts on `http://localhost:3000`.

## Try It

Public route:

```bash
curl http://localhost:3000/
```

Protected refund route:

```bash
curl -X POST http://localhost:3000/refund \
-H "content-type: application/json" \
-H "x-agent-id: agt_demo_refund_bot" \
-d '{"amount":100}'
```

Protected admin route:

```bash
curl http://localhost:3000/admin \
-H "x-agent-id: agt_demo_admin_bot"
```

## Integration Highlight

Add APort verification to Hono routes by creating a middleware factory and applying a policy pack before the route handler:

```javascript
import { Hono } from "hono";
import { createAPortHonoMiddleware } from "./src/index.js";

const app = new Hono();
const aport = createAPortHonoMiddleware({
apiKey: process.env.APORT_API_KEY,
});

app.post("/refund", aport("finance.payment.refund.v1"), c => {
const aportResult = c.get("aport");
return c.json({ success: true, agentId: aportResult.agentId });
});
```

## Using The Real SDK

This example ships with a mock client so tests and demos work out of the box. In production, inject a client backed by `@aporthq/sdk-node`:

```javascript
import { APortClient } from "@aporthq/sdk-node";
import { createAPortHonoMiddleware } from "./src/index.js";

const aportClient = new APortClient({
apiKey: process.env.APORT_API_KEY,
baseUrl: process.env.APORT_BASE_URL,
});

const aport = createAPortHonoMiddleware({ client: aportClient });
```

Your injected client only needs a compatible `verify(policy, agentId, context)` method.

## Agent ID Sources

The middleware checks these sources in order:

1. `X-Agent-ID` header
2. `X-APort-Agent-ID` header
3. `agent_id` query parameter
4. `agent_id` JSON body field
5. `agentId` JSON body field
6. Static `agentId` middleware option

## Configuration

```bash
cp .env.example .env
```

Environment variables:

- `APORT_API_KEY`: APort API key for a real client
- `APORT_BASE_URL`: Optional APort API base URL
- `PORT`: Local server port

## Tests

```bash
npm test
```

The test suite covers successful verification, missing agent IDs, denied agents, strict fail-closed behavior, non-strict fallback behavior, and agent ID extraction.
68 changes: 68 additions & 0 deletions examples/frameworks/hono/examples/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { serve } from "@hono/node-server";
import { Hono } from "hono";

import { createAPortHonoMiddleware } from "../src/index.js";

const app = new Hono();
const aport = createAPortHonoMiddleware({
apiKey: process.env.APORT_API_KEY,
baseUrl: process.env.APORT_BASE_URL,
});

app.get("/", c =>
c.json({
message: "APort Hono example",
routes: {
refund: "POST /refund",
admin: "GET /admin",
},
}),
);

app.post("/refund", aport("finance.payment.refund.v1"), c => {
const body = c.get("aportBody") || {};
const aportResult = c.get("aport");
const maxRefund = aportResult.passport?.limits?.refund_amount_max_per_tx ?? 0;

if (Number(body.amount || 0) > maxRefund) {
return c.json(
{
error: "Refund exceeds policy limit",
requested: body.amount,
limit: maxRefund,
},
403,
);
}

return c.json({
success: true,
agentId: aportResult.agentId,
policy: aportResult.policy,
amount: body.amount,
});
});

app.get(
"/admin",
aport("admin.access", {
context: { endpoint: "admin" },
}),
c => {
const aportResult = c.get("aport");
return c.json({
success: true,
agentId: aportResult.agentId,
capabilities: aportResult.passport?.capabilities || [],
});
},
);

const port = Number(process.env.PORT || 3000);

serve({
fetch: app.fetch,
port,
});

console.log(`APort Hono example listening on http://localhost:${port}`);
41 changes: 41 additions & 0 deletions examples/frameworks/hono/package-lock.json

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

27 changes: 27 additions & 0 deletions examples/frameworks/hono/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "aport-hono-framework-example",
"version": "1.0.0",
"description": "Runnable APort verification example for Hono applications",
"type": "module",
"main": "src/index.js",
"scripts": {
"start": "node examples/server.js",
"test": "node --test"
},
"keywords": [
"aport",
"hono",
"middleware",
"agent-verification",
"policy"
],
"author": "APort Community",
"license": "MIT",
"dependencies": {
"hono": "^4.6.16",
"@hono/node-server": "^1.13.8"
},
"engines": {
"node": ">=18.0.0"
}
}
123 changes: 123 additions & 0 deletions examples/frameworks/hono/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
class MockAPortClient {
constructor(options = {}) {
this.apiKey = options.apiKey || process.env.APORT_API_KEY;
this.baseUrl = options.baseUrl || process.env.APORT_BASE_URL || "https://api.aport.io";
}

async verify(policy, agentId, context = {}) {
return {
verified: true,
passport: {
agentId,
capabilities: ["finance.payment.refund.v1"],
limits: { refund_amount_max_per_tx: 1000 },
},
policy,
context,
message: "Mock verification successful",
};
}
}

async function readJsonBody(c) {
const contentType = c.req.header("content-type") || "";
if (!contentType.includes("application/json")) {
return {};
}

try {
return await c.req.json();
} catch {
return {};
}
}

function extractAgentId(c, options = {}) {
const body = c.get("aportBody") || {};
const candidates = [
c.req.header("x-agent-id"),
c.req.header("x-aport-agent-id"),
c.req.query("agent_id"),
body.agent_id,
body.agentId,
options.agentId,
];

return candidates.find(value => typeof value === "string" && value.length > 0) || null;
}

function createAPortHonoMiddleware(options = {}) {
const client =
options.client ||
new MockAPortClient({
apiKey: options.apiKey,
baseUrl: options.baseUrl,
});

return function requirePolicy(policy, middlewareOptions = {}) {
return async function aportPolicyMiddleware(c, next) {
const body = await readJsonBody(c);
c.set("aportBody", body);

const agentId = extractAgentId(c, middlewareOptions);
if (!agentId) {
return c.json(
{
error: "Agent ID required",
message: "Provide X-Agent-ID, X-APort-Agent-ID, agent_id, or agentId.",
},
400,
);
}

try {
const result = await client.verify(policy, agentId, {
method: c.req.method,
path: new URL(c.req.url).pathname,
...middlewareOptions.context,
});

if (!result.verified) {
return c.json(
{
error: "Verification failed",
message: result.message || "Agent verification failed",
details: result.details,
},
403,
);
}

c.set("aport", {
verified: true,
agentId,
passport: result.passport,
policy,
result,
});

return next();
} catch (error) {
if (middlewareOptions.strict === false) {
c.set("aport", {
verified: false,
agentId,
policy,
error: error.message,
});
return next();
}

return c.json(
{
error: "Verification error",
message: "Internal verification error",
},
500,
);
}
};
};
}

export { MockAPortClient, createAPortHonoMiddleware, extractAgentId };
Loading