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
87 changes: 87 additions & 0 deletions QUICKSTART.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# AgentPass — Підключи за 5 хвилин

## Що це?
Identity layer для AI агентів. Твій агент отримує криптографічний паспорт (Ed25519), може верифікувати себе і отримувати email.

## 1. Встанови SDK

```bash
npm install @agentpass/sdk
```

## 2. Створи паспорт

```typescript
import { AgentPassClient } from '@agentpass/sdk';

const client = new AgentPassClient({
apiUrl: 'https://api.agentpass.space'
});

// Реєстрація (одноразово)
const { token } = await client.register({
email: '[email protected]',
password: 'secure-password',
name: 'My Agent'
});

// Створення паспорта
const passport = await client.createPassport({
name: 'my-agent',
capabilities: ['code', 'web']
});

console.log('Passport ID:', passport.passport_id);
// → ap_xxxxxxxxxxxx
console.log('Email:', passport.email);
// → [email protected]
```

## 3. Верифікуй себе

```typescript
// Будь-який інший агент або сервіс може перевірити тебе:
const result = await client.verify({
passport_id: 'ap_xxxxxxxxxxxx',
challenge: 'random-string-from-verifier',
signature: client.sign('random-string-from-verifier')
});

console.log(result.valid); // true
console.log(result.trust_score); // 0-100
```

## 4. Що далі?

- **Email:** Твій агент автоматично отримує `[email protected]`
- **Trust Score:** Росте з кожною успішною верифікацією
- **MCP Server:** 17 тулів для OpenClaw/Claude Desktop — `npx @agentpass/mcp-server serve`
- **Fallback Auth:** Автоматична реєстрація на сторонніх сервісах через браузер

## API (curl)

```bash
# Реєстрація
curl -X POST https://api.agentpass.space/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"pass","name":"Agent"}'

# Створення паспорта
curl -X POST https://api.agentpass.space/passports \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"my-agent","public_key":"BASE64_ED25519_KEY","capabilities":["code"]}'

# Верифікація
curl -X POST https://api.agentpass.space/verify \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"passport_id":"ap_xxx","challenge":"test","signature":"BASE64_SIG"}'
```

## Посилання

- API: https://api.agentpass.space
- Dashboard: https://dashboard.agentpass.space
- GitHub: https://github.com/kai-agent-free/AgentPass
- NPM: `@agentpass/sdk`
1 change: 1 addition & 0 deletions packages/api-server/.env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DATABASE_URL="postgresql://root:root@localhost:5432/agentpass_test"
1 change: 1 addition & 0 deletions packages/api-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@agentpass/core": "workspace:*",
"@hono/node-server": "^1.13.8",
"@hono/node-ws": "^1.3.0",
"@profullstack/coinpay": "0.6.10",
"bcryptjs": "^3.0.3",
"hono": "^4.7.4",
"jose": "^6.1.3",
Expand Down
8 changes: 8 additions & 0 deletions packages/api-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import { createWebhookRouter } from "./routes/webhooks.js";
import { createTelegramRouter } from "./routes/telegram.js";
import { createMessagesRouter } from "./routes/messages.js";
import { createSettingsRouter } from "./routes/settings.js";
import { createReputationRouter } from "./routes/reputation.js";
import { createHealthRouter } from "./middleware/health.js";
import { rateLimiters } from "./middleware/rate-limiter.js";
import { requestLogger } from "./middleware/request-logging.js";
import { sanitizeBody } from "./middleware/sanitize-body.js";
import { createDemoApp } from "./demo/demo-app.js";

const PORT = parseInt(process.env.AGENTPASS_PORT || "3846", 10);
Expand Down Expand Up @@ -62,6 +64,9 @@ export async function createApp(connectionString: string = DATABASE_URL): Promis
allowHeaders: ['Content-Type', 'Authorization', 'X-Webhook-Secret', 'X-AgentPass-ID', 'X-AgentPass-Signature', 'X-Request-ID'],
}));

// Sanitize request bodies against prototype pollution
app.use("*", sanitizeBody());

// Apply default rate limiting to all routes
app.use("*", rateLimiters.default);

Expand Down Expand Up @@ -97,6 +102,7 @@ export async function createApp(connectionString: string = DATABASE_URL): Promis
const messagesRouter = createMessagesRouter(db);
const settingsRouter = createSettingsRouter(db);
const telegramRouter = createTelegramRouter(db);
const reputationRouter = createReputationRouter(db);
const healthRouter = createHealthRouter(db);

app.route("/", healthRouter);
Expand Down Expand Up @@ -124,6 +130,8 @@ export async function createApp(connectionString: string = DATABASE_URL): Promis
app.route("/settings", settingsRouter);
// Telegram routes for bot webhooks and account linking
app.route("/telegram", telegramRouter);
// DID reputation via CoinPay Reputation Protocol
app.route("/reputation", reputationRouter);

// Demo service — "Login with AgentPass" native auth showcase
const { app: demoApp } = createDemoApp(
Expand Down
11 changes: 2 additions & 9 deletions packages/api-server/src/middleware/health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,10 @@ describe("Health endpoints", () => {
expect(data.status).toBe("ok");
});

it("includes version field", async () => {
it("does not leak version or uptime", async () => {
const res = await app.request("/health");
const data = (await res.json()) as Record<string, unknown>;
expect(data.version).toBe("0.1.0");
});

it("includes uptime_seconds as a number", async () => {
const res = await app.request("/health");
const data = (await res.json()) as Record<string, unknown>;
expect(typeof data.uptime_seconds).toBe("number");
expect(data.uptime_seconds).toBeGreaterThanOrEqual(0);
expect(data).toEqual({ status: "ok" });
});
});

Expand Down
10 changes: 1 addition & 9 deletions packages/api-server/src/middleware/health.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@
import { Hono } from "hono";
import type { Sql } from "../db/schema.js";

const VERSION = "0.1.0";
const startTime = Date.now();

/**
* Create the health-check router.
*
Expand All @@ -20,12 +17,7 @@ export function createHealthRouter(db: Sql) {
const router = new Hono();

router.get("/health", (c) => {
const uptimeSeconds = Math.floor((Date.now() - startTime) / 1000);
return c.json({
status: "ok",
version: VERSION,
uptime_seconds: uptimeSeconds,
});
return c.json({ status: "ok" });
});

router.get("/ready", async (c) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/api-server/src/middleware/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ export const rateLimiters = {

/** Default for other endpoints: 60 requests per minute */
default: rateLimiter({ maxRequests: 60, windowMs: 60 * 1000 }),

/** Auth endpoints (login/register): 5 requests per minute */
auth: rateLimiter({ maxRequests: 5, windowMs: 60 * 1000 }),
};

/**
Expand Down
46 changes: 46 additions & 0 deletions packages/api-server/src/middleware/sanitize-body.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* Middleware to strip prototype pollution keys from request bodies.
*
* Recursively removes __proto__, constructor, and prototype keys
* from parsed JSON bodies to prevent prototype pollution attacks.
*/

import type { Context, Next } from "hono";

const DANGEROUS_KEYS = new Set(["__proto__", "constructor", "prototype"]);

/**
* Recursively strip dangerous keys from an object.
*/
function stripDangerousKeys(obj: unknown): unknown {
if (obj === null || typeof obj !== "object") return obj;
if (Array.isArray(obj)) return obj.map(stripDangerousKeys);

const clean: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
if (DANGEROUS_KEYS.has(key)) continue;
clean[key] = stripDangerousKeys(value);
}
return clean;
}

/**
* Hono middleware that sanitizes request bodies against prototype pollution.
*/
export function sanitizeBody() {
return async (c: Context, next: Next) => {
if (c.req.header("content-type")?.includes("application/json")) {
try {
const body = await c.req.json();
const sanitized = stripDangerousKeys(body);
// Replace the req.json() method to return sanitized body
c.req.json = async <T = unknown>(): Promise<T> => {
return sanitized as T;
};
} catch {
// If body isn't valid JSON, let downstream handlers deal with it
}
}
await next();
};
}
4 changes: 2 additions & 2 deletions packages/api-server/src/routes/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ describe("Auth routes", () => {
it("returns 409 for duplicate email", async () => {
await register();
const res = await register();
expect(res.status).toBe(409);
expect(res.status).toBe(400);

const data = await res.json();
expect(data.code).toBe("EMAIL_EXISTS");
expect(data.code).toBe("REGISTRATION_FAILED");
});

it("returns 400 for invalid email", async () => {
Expand Down
13 changes: 8 additions & 5 deletions packages/api-server/src/routes/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ import type { Sql } from "../db/schema.js";
import bcrypt from "bcryptjs";
import { zValidator, getValidatedBody } from "../middleware/validation.js";
import { signJwt, requireAuth, type OwnerPayload, type AuthVariables } from "../middleware/auth.js";
import { rateLimiters } from "../middleware/rate-limiter.js";

// --- Zod schemas ---

const RegisterSchema = z.object({
email: z.string().email("Invalid email address"),
password: z
.string()
.min(8, "Password must be at least 8 characters")
.min(12, "Password must be at least 12 characters")
.regex(/[0-9]/, "Password must contain at least one number")
.regex(/[^a-zA-Z0-9]/, "Password must contain at least one special character")
.max(128, "Password must be 128 characters or fewer"),
name: z
.string()
Expand Down Expand Up @@ -60,7 +63,7 @@ export function createAuthRouter(db: Sql): Hono<{ Variables: AuthVariables }> {
const router = new Hono<{ Variables: AuthVariables }>();

// POST /auth/register — Create owner account
router.post("/register", zValidator(RegisterSchema), async (c) => {
router.post("/register", rateLimiters.auth, zValidator(RegisterSchema), async (c) => {
const body = getValidatedBody<RegisterBody>(c);

// Check if email already exists
Expand All @@ -70,8 +73,8 @@ export function createAuthRouter(db: Sql): Hono<{ Variables: AuthVariables }> {

if (existing.length > 0) {
return c.json(
{ error: "Email already registered", code: "EMAIL_EXISTS" },
409,
{ error: "Registration failed", code: "REGISTRATION_FAILED" },
400,
);
}

Expand Down Expand Up @@ -104,7 +107,7 @@ export function createAuthRouter(db: Sql): Hono<{ Variables: AuthVariables }> {
});

// POST /auth/login — Login and get JWT
router.post("/login", zValidator(LoginSchema), async (c) => {
router.post("/login", rateLimiters.auth, zValidator(LoginSchema), async (c) => {
const body = getValidatedBody<LoginBody>(c);

// Look up owner by email
Expand Down
Loading