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: 'your-agent@agent-mail.xyz',
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);
// → my-agent@agent-mail.xyz
```

## 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:** Твій агент автоматично отримує `name@agent-mail.xyz`
- **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":"agent@agent-mail.xyz","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
4 changes: 4 additions & 0 deletions packages/api-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ 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";
Expand Down Expand Up @@ -97,6 +98,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 +126,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
168 changes: 168 additions & 0 deletions packages/api-server/src/routes/reputation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
/**
* DID Reputation routes — powered by CoinPay Reputation Protocol.
*
* GET /reputation/:did — Get reputation for a DID
* GET /reputation/:did/trust — Get trust vector {E,P,B,D,R,A,C}
* GET /reputation/:did/badge — Proxy badge SVG from CoinPay
* POST /reputation/receipt — Submit a task receipt (requires auth)
* GET /reputation/did/me — Get this platform's DID (requires auth)
* POST /reputation/did/claim — Claim a DID (requires auth)
* POST /reputation/issuer — Register as platform issuer (requires auth)
*/

import { Hono } from "hono";
import type { Sql } from "../db/schema.js";
import { requireAuth, type AuthVariables } from "../middleware/auth.js";

// Lazy-load SDK to avoid subpath export issues
let _coinpay: any = null;
let _reputation: any = null;

async function loadSDK() {
if (!_coinpay) {
_coinpay = await import("@profullstack/coinpay");
}
if (!_reputation) {
// The reputation subpath isn't in the exports map, so resolve manually
const { createRequire } = await import("node:module");
const req = createRequire(import.meta.url);
const path = req.resolve("@profullstack/coinpay/src/reputation.js");
_reputation = await import(path);
}
return { coinpay: _coinpay, reputation: _reputation };
}

function getCoinPayClient() {
const apiKey = process.env.COINPAY_API_KEY;
if (!apiKey) {
throw new Error("COINPAY_API_KEY environment variable is not set");
}
const { CoinPayClient } = _coinpay;
return new CoinPayClient({
apiKey,
baseUrl: process.env.COINPAY_BASE_URL || "https://coinpayportal.com/api",
});
}

export function createReputationRouter(db: Sql) {
const app = new Hono<{ Variables: AuthVariables }>();
const auth = requireAuth(db);

// Ensure SDK is loaded before handling requests
app.use("*", async (_c, next) => {
await loadSDK();
return next();
});

// --- Public endpoints ---

app.get("/:did", async (c) => {
const did = decodeURIComponent(c.req.param("did"));
try {
const client = getCoinPayClient();
const result = await _reputation.getReputation(client, did);
return c.json(result);
} catch (err: any) {
if (err.message?.includes("COINPAY_API_KEY")) {
return c.json({ error: "Reputation service not configured" }, 503);
}
return c.json({ error: "Failed to fetch reputation", detail: err.message }, 502);
}
});

app.get("/:did/trust", async (c) => {
const did = decodeURIComponent(c.req.param("did"));
try {
const client = getCoinPayClient();
const result = await _reputation.getTrustProfile(client, did);
return c.json(result);
} catch (err: any) {
if (err.message?.includes("COINPAY_API_KEY")) {
return c.json({ error: "Reputation service not configured" }, 503);
}
return c.json({ error: "Failed to fetch trust profile", detail: err.message }, 502);
}
});

app.get("/:did/badge", async (c) => {
const did = decodeURIComponent(c.req.param("did"));
const baseUrl = process.env.COINPAY_BASE_URL || "https://coinpayportal.com/api";
const badgeUrl = _reputation.getBadgeUrl(baseUrl, did);

try {
const res = await fetch(badgeUrl, {
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) {
return c.json({ error: "Badge not available" }, res.status as any);
}
const svg = await res.text();
c.header("Content-Type", "image/svg+xml");
c.header("Cache-Control", "public, max-age=300");
return c.body(svg);
} catch {
return c.json({ error: "Badge fetch failed" }, 502);
}
});

// --- Authenticated endpoints ---

app.post("/receipt", auth, async (c) => {
try {
const client = getCoinPayClient();
const receipt = await c.req.json();
const result = await _reputation.submitReceipt(client, receipt);
return c.json(result);
} catch (err: any) {
if (err.message?.includes("COINPAY_API_KEY")) {
return c.json({ error: "Reputation service not configured" }, 503);
}
return c.json({ error: "Failed to submit receipt", detail: err.message }, 502);
}
});

app.get("/did/me", auth, async (c) => {
try {
const client = getCoinPayClient();
const result = await _reputation.getMyDid(client);
return c.json(result);
} catch (err: any) {
if (err.message?.includes("COINPAY_API_KEY")) {
return c.json({ error: "Reputation service not configured" }, 503);
}
return c.json({ error: "Failed to get DID", detail: err.message }, 502);
}
});

app.post("/did/claim", auth, async (c) => {
try {
const client = getCoinPayClient();
const result = await _reputation.claimDid(client);
return c.json(result);
} catch (err: any) {
if (err.message?.includes("COINPAY_API_KEY")) {
return c.json({ error: "Reputation service not configured" }, 503);
}
return c.json({ error: "Failed to claim DID", detail: err.message }, 502);
}
});

app.post("/issuer", auth, async (c) => {
try {
const client = getCoinPayClient();
const body = await c.req.json();
const result = await _reputation.registerPlatformIssuer(client, {
name: body.name || "AgentPass",
domain: body.domain || "agentpass.space",
});
return c.json(result);
} catch (err: any) {
if (err.message?.includes("COINPAY_API_KEY")) {
return c.json({ error: "Reputation service not configured" }, 503);
}
return c.json({ error: "Failed to register issuer", detail: err.message }, 502);
}
});

return app;
}
2 changes: 1 addition & 1 deletion packages/api-server/src/routes/trust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ export function createTrustRouter(db: Sql): Hono<{ Variables: AuthVariables }> {

(metadata.external_attestations as ExternalAttestationFactor[]).push(attestation);

const { score, level, factors } = await recalculateAndPersist(
const { score, level, factors: _factors3 } = await recalculateAndPersist(
passportId,
metadata,
row.created_at,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export * from "./crypto/index.js";
export * from "./passport/index.js";
export * from "./vault/index.js";
export * from "./types/index.js";
export * from "./reputation/index.js";
export * from "./errors.js";
export { createLogger, sanitize, type Logger } from "./logger.js";
59 changes: 59 additions & 0 deletions packages/core/src/reputation/aggregator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { describe, it, expect } from "vitest";
import { ReputationAggregator } from "./aggregator.js";
import type { ReputationProvider, ReputationData } from "./types.js";

const mockProvider: ReputationProvider = {
name: "mock",
async fetchReputation(did: string): Promise<ReputationData> {
return {
did,
provider: "mock",
dimensions: { economic: 80, behavioral: 90 },
compositeScore: 85,
fetchedAt: "2026-03-15T00:00:00Z",
};
},
async verifyOwnership(): Promise<boolean> {
return true;
},
};

const failingProvider: ReputationProvider = {
name: "failing",
async fetchReputation(): Promise<ReputationData> {
throw new Error("network error");
},
async verifyOwnership(): Promise<boolean> {
return false;
},
};

describe("ReputationAggregator", () => {
it("should register and fetch from providers", async () => {
const agg = new ReputationAggregator();
agg.register(mockProvider);

const results = await agg.fetchAll("did:key:z6Mktest");
expect(results).toHaveLength(1);
expect(results[0].source).toBe("did:reputation:mock");
expect(results[0].score).toBe(85);
expect(results[0].attester_id).toBe("did:key:z6Mktest");
});

it("should handle provider failures gracefully", async () => {
const agg = new ReputationAggregator();
agg.register(mockProvider);
agg.register(failingProvider);

const results = await agg.fetchAll("did:key:z6Mktest");
// Should still return the successful one
expect(results).toHaveLength(1);
expect(results[0].source).toBe("did:reputation:mock");
});

it("should return empty array with no providers", async () => {
const agg = new ReputationAggregator();
const results = await agg.fetchAll("did:key:z6Mktest");
expect(results).toHaveLength(0);
});
});
Loading