piree-merchant-middleware 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ export { pireeAgentVerifier } from './middleware.js';
2
+ export type { VerifierOptions } from './middleware.js';
3
+ export type { AgentRegistryEntry } from './registry.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { pireeAgentVerifier } from './middleware.js';
@@ -0,0 +1,15 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import fetch from 'node-fetch';
3
+ import { AgentRegistryEntry } from './registry.js';
4
+ type FetchFn = typeof fetch;
5
+ export interface VerifierOptions {
6
+ registryUrl: string;
7
+ header?: string;
8
+ refreshIntervalMs?: number;
9
+ onUnknownAgent?: 'reject' | 'allow' | 'log';
10
+ _fetchFn?: FetchFn;
11
+ }
12
+ export declare function pireeAgentVerifier(options: VerifierOptions): (req: Request & {
13
+ pireeAgent?: AgentRegistryEntry | null;
14
+ }, res: Response, next: NextFunction) => void;
15
+ export {};
@@ -0,0 +1,40 @@
1
+ import fetch from 'node-fetch';
2
+ import { fetchFullRegistry } from './registry.js';
3
+ export function pireeAgentVerifier(options) {
4
+ const { registryUrl, header = 'Piree-Agent-Id', refreshIntervalMs = 5 * 60 * 1000, onUnknownAgent = 'reject', _fetchFn = fetch, } = options;
5
+ let registry = new Map();
6
+ const headerLower = header.toLowerCase();
7
+ async function refresh() {
8
+ try {
9
+ registry = await fetchFullRegistry(registryUrl, _fetchFn);
10
+ }
11
+ catch (err) {
12
+ console.error('[piree-middleware] Registry refresh failed:', err.message);
13
+ }
14
+ }
15
+ refresh().then(() => {
16
+ setInterval(refresh, refreshIntervalMs).unref();
17
+ }).catch(err => {
18
+ console.error('[piree-middleware] Initial registry fetch failed:', err.message);
19
+ });
20
+ return function middleware(req, res, next) {
21
+ const agentId = req.headers[headerLower];
22
+ if (!agentId) {
23
+ return next();
24
+ }
25
+ const agent = registry.get(agentId);
26
+ if (agent) {
27
+ req.pireeAgent = agent;
28
+ return next();
29
+ }
30
+ if (onUnknownAgent === 'reject') {
31
+ res.status(401).json({ error: 'agent_not_trusted' });
32
+ return;
33
+ }
34
+ if (onUnknownAgent === 'log') {
35
+ console.warn(`[piree-middleware] Unverified agent: ${agentId}`);
36
+ }
37
+ req.pireeAgent = null;
38
+ return next();
39
+ };
40
+ }
@@ -0,0 +1,17 @@
1
+ import fetch from 'node-fetch';
2
+ import type { RequestInfo, RequestInit } from 'node-fetch';
3
+ export interface AgentRegistryEntry {
4
+ credential_id: string;
5
+ agent_name: string;
6
+ org_did: string;
7
+ purpose: string;
8
+ scope_summary: {
9
+ categories: string[];
10
+ spend_limit_monthly: number;
11
+ };
12
+ agent_card_url: string;
13
+ valid_until: string;
14
+ }
15
+ type FetchFn = (url: RequestInfo, init?: RequestInit) => ReturnType<typeof fetch>;
16
+ export declare function fetchFullRegistry(baseUrl: string, fetchFn?: FetchFn): Promise<Map<string, AgentRegistryEntry>>;
17
+ export {};
@@ -0,0 +1,21 @@
1
+ import fetch from 'node-fetch';
2
+ export async function fetchFullRegistry(baseUrl, fetchFn = fetch) {
3
+ const registry = new Map();
4
+ let cursor = null;
5
+ do {
6
+ const url = new URL(`${baseUrl}/registry/v1/trusted-agents`);
7
+ if (cursor)
8
+ url.searchParams.set('cursor', cursor);
9
+ url.searchParams.set('limit', '100');
10
+ const resp = await fetchFn(url.toString());
11
+ if (!resp.ok) {
12
+ throw new Error(`Registry fetch failed with status ${resp.status}`);
13
+ }
14
+ const data = await resp.json();
15
+ for (const agent of data.agents) {
16
+ registry.set(agent.credential_id, agent);
17
+ }
18
+ cursor = data.next_cursor;
19
+ } while (cursor);
20
+ return registry;
21
+ }
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "piree-merchant-middleware",
3
+ "version": "1.0.0",
4
+ "description": "Express middleware that verifies Piree agent credentials against the public trusted-agents registry",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "piree",
21
+ "agent",
22
+ "middleware",
23
+ "express",
24
+ "trust"
25
+ ],
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "node-fetch": "^3.3.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/express": "^5.0.0",
32
+ "@types/jest": "^29.5.14",
33
+ "@types/node": "^20.0.0",
34
+ "@types/supertest": "^7.2.0",
35
+ "cross-env": "^10.1.0",
36
+ "express": "^5.0.0",
37
+ "jest": "^30.4.0",
38
+ "supertest": "^7.2.2",
39
+ "ts-jest": "^29.4.11",
40
+ "typescript": "^5.4.0"
41
+ },
42
+ "jest": {
43
+ "preset": "ts-jest/presets/default-esm",
44
+ "testEnvironment": "node",
45
+ "extensionsToTreatAsEsm": [
46
+ ".ts"
47
+ ],
48
+ "moduleNameMapper": {
49
+ "^(\\.{1,2}/.*)\\.js$": "$1"
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,104 @@
1
+ import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals'
2
+ import express, { Response } from 'express'
3
+ import supertest from 'supertest'
4
+ import { pireeAgentVerifier } from '../middleware.js'
5
+
6
+ describe('pireeAgentVerifier middleware', () => {
7
+ function makeRegistryPage(agents: object[], nextCursor: string | null = null) {
8
+ const mockFetch = jest.fn<any>().mockResolvedValue({
9
+ ok: true,
10
+ json: jest.fn<any>().mockResolvedValue({
11
+ registry: 'piree.ai',
12
+ total: agents.length,
13
+ agents,
14
+ next_cursor: nextCursor,
15
+ }),
16
+ })
17
+ return mockFetch
18
+ }
19
+
20
+ function makeAgent(credentialId: string, agentName = 'test-agent') {
21
+ return {
22
+ credential_id: credentialId,
23
+ agent_name: agentName,
24
+ org_did: 'did:web:test.com',
25
+ purpose: 'autonomous-commerce-agent',
26
+ scope_summary: { categories: ['grocery'], spend_limit_monthly: 500000 },
27
+ agent_card_url: `https://api.piree.ai/api/v1/credentials/${credentialId}/agent-card`,
28
+ valid_until: '2027-06-07T00:00:00Z',
29
+ }
30
+ }
31
+
32
+ function buildApp(mockFetch: jest.Mock<any>, options: object) {
33
+ const app = express()
34
+ app.use(pireeAgentVerifier({ registryUrl: 'https://api.piree.ai', _fetchFn: mockFetch as any, ...options }))
35
+ app.get('/test', (req: any, res: Response) =>
36
+ res.json({ agent: req.pireeAgent?.agent_name ?? null })
37
+ )
38
+ return app
39
+ }
40
+
41
+ it('passes request and populates req.pireeAgent for known agent', async () => {
42
+ const CRED_ID = 'cred-123'
43
+ const mockFetch = makeRegistryPage([makeAgent(CRED_ID)])
44
+ const app = buildApp(mockFetch, { header: 'Piree-Agent-Id' })
45
+ await new Promise(resolve => setTimeout(resolve, 50))
46
+
47
+ const res = await supertest(app)
48
+ .get('/test')
49
+ .set('Piree-Agent-Id', CRED_ID)
50
+
51
+ expect(res.status).toBe(200)
52
+ expect(res.body.agent).toBe('test-agent')
53
+ })
54
+
55
+ it('returns 401 for unknown agent when onUnknownAgent=reject', async () => {
56
+ const mockFetch = makeRegistryPage([])
57
+ const app = buildApp(mockFetch, { onUnknownAgent: 'reject' })
58
+ await new Promise(resolve => setTimeout(resolve, 50))
59
+
60
+ const res = await supertest(app)
61
+ .get('/test')
62
+ .set('Piree-Agent-Id', 'unknown-cred-id')
63
+
64
+ expect(res.status).toBe(401)
65
+ expect(res.body.error).toBe('agent_not_trusted')
66
+ })
67
+
68
+ it('passes request with pireeAgent=null when onUnknownAgent=allow', async () => {
69
+ const mockFetch = makeRegistryPage([])
70
+ const app = buildApp(mockFetch, { onUnknownAgent: 'allow' })
71
+ await new Promise(resolve => setTimeout(resolve, 50))
72
+
73
+ const res = await supertest(app)
74
+ .get('/test')
75
+ .set('Piree-Agent-Id', 'unknown-cred')
76
+
77
+ expect(res.status).toBe(200)
78
+ expect(res.body.agent).toBeNull()
79
+ })
80
+
81
+ it('passes request normally when no Piree-Agent-Id header present', async () => {
82
+ const mockFetch = makeRegistryPage([])
83
+ const app = buildApp(mockFetch, {})
84
+ await new Promise(resolve => setTimeout(resolve, 50))
85
+
86
+ const res = await supertest(app).get('/test')
87
+ expect(res.status).toBe(200)
88
+ })
89
+
90
+ it('fails open when initial registry fetch fails', async () => {
91
+ const warnSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
92
+ const mockFetch = jest.fn<any>().mockRejectedValue(new Error('Network error'))
93
+ const app = buildApp(mockFetch, { onUnknownAgent: 'reject' })
94
+ await new Promise(resolve => setTimeout(resolve, 50))
95
+
96
+ const res = await supertest(app)
97
+ .get('/test')
98
+ .set('Piree-Agent-Id', 'any-cred')
99
+
100
+ // Registry failed to load → empty registry → unknown agent → 401 (onUnknownAgent=reject)
101
+ expect(res.status).toBe(401)
102
+ warnSpy.mockRestore()
103
+ })
104
+ })
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { pireeAgentVerifier } from './middleware.js'
2
+ export type { VerifierOptions } from './middleware.js'
3
+ export type { AgentRegistryEntry } from './registry.js'
@@ -0,0 +1,71 @@
1
+ import { Request, Response, NextFunction } from 'express'
2
+ import fetch from 'node-fetch'
3
+ import { fetchFullRegistry, AgentRegistryEntry } from './registry.js'
4
+
5
+ type FetchFn = typeof fetch
6
+
7
+ export interface VerifierOptions {
8
+ registryUrl: string
9
+ header?: string
10
+ refreshIntervalMs?: number
11
+ onUnknownAgent?: 'reject' | 'allow' | 'log'
12
+ _fetchFn?: FetchFn // DI for testing — do not use in production
13
+ }
14
+
15
+ export function pireeAgentVerifier(options: VerifierOptions) {
16
+ const {
17
+ registryUrl,
18
+ header = 'Piree-Agent-Id',
19
+ refreshIntervalMs = 5 * 60 * 1000,
20
+ onUnknownAgent = 'reject',
21
+ _fetchFn = fetch,
22
+ } = options
23
+
24
+ let registry = new Map<string, AgentRegistryEntry>()
25
+ const headerLower = header.toLowerCase()
26
+
27
+ async function refresh(): Promise<void> {
28
+ try {
29
+ registry = await fetchFullRegistry(registryUrl, _fetchFn)
30
+ } catch (err) {
31
+ console.error('[piree-middleware] Registry refresh failed:', (err as Error).message)
32
+ }
33
+ }
34
+
35
+ refresh().then(() => {
36
+ setInterval(refresh, refreshIntervalMs).unref()
37
+ }).catch(err => {
38
+ console.error('[piree-middleware] Initial registry fetch failed:', (err as Error).message)
39
+ })
40
+
41
+ return function middleware(
42
+ req: Request & { pireeAgent?: AgentRegistryEntry | null },
43
+ res: Response,
44
+ next: NextFunction
45
+ ): void {
46
+ const agentId = req.headers[headerLower] as string | undefined
47
+
48
+ if (!agentId) {
49
+ return next()
50
+ }
51
+
52
+ const agent = registry.get(agentId)
53
+
54
+ if (agent) {
55
+ req.pireeAgent = agent
56
+ return next()
57
+ }
58
+
59
+ if (onUnknownAgent === 'reject') {
60
+ res.status(401).json({ error: 'agent_not_trusted' })
61
+ return
62
+ }
63
+
64
+ if (onUnknownAgent === 'log') {
65
+ console.warn(`[piree-middleware] Unverified agent: ${agentId}`)
66
+ }
67
+
68
+ req.pireeAgent = null
69
+ return next()
70
+ }
71
+ }
@@ -0,0 +1,44 @@
1
+ import fetch from 'node-fetch'
2
+ import type { RequestInfo, RequestInit } from 'node-fetch'
3
+
4
+ export interface AgentRegistryEntry {
5
+ credential_id: string
6
+ agent_name: string
7
+ org_did: string
8
+ purpose: string
9
+ scope_summary: { categories: string[]; spend_limit_monthly: number }
10
+ agent_card_url: string
11
+ valid_until: string
12
+ }
13
+
14
+ type FetchFn = (url: RequestInfo, init?: RequestInit) => ReturnType<typeof fetch>
15
+
16
+ export async function fetchFullRegistry(
17
+ baseUrl: string,
18
+ fetchFn: FetchFn = fetch
19
+ ): Promise<Map<string, AgentRegistryEntry>> {
20
+ const registry = new Map<string, AgentRegistryEntry>()
21
+ let cursor: string | null = null
22
+
23
+ do {
24
+ const url = new URL(`${baseUrl}/registry/v1/trusted-agents`)
25
+ if (cursor) url.searchParams.set('cursor', cursor)
26
+ url.searchParams.set('limit', '100')
27
+
28
+ const resp = await fetchFn(url.toString())
29
+ if (!resp.ok) {
30
+ throw new Error(`Registry fetch failed with status ${resp.status}`)
31
+ }
32
+ const data = await resp.json() as {
33
+ agents: AgentRegistryEntry[]
34
+ next_cursor: string | null
35
+ }
36
+
37
+ for (const agent of data.agents) {
38
+ registry.set(agent.credential_id, agent)
39
+ }
40
+ cursor = data.next_cursor
41
+ } while (cursor)
42
+
43
+ return registry
44
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "declaration": true,
8
+ "strict": true,
9
+ "isolatedModules": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src/**/*.ts"],
14
+ "exclude": ["src/**/__tests__/**"]
15
+ }