imarobot-verify 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.
- package/README.md +52 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +206 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# imarobot-verify
|
|
2
|
+
|
|
3
|
+
Verify [ImaRobot](https://imarobot.ai) agent identity tokens. Open source receiver-side SDK.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install imarobot-verify
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
const { createVerifier } = require('imarobot-verify');
|
|
15
|
+
|
|
16
|
+
const verifier = createVerifier({
|
|
17
|
+
publishableKey: 'pk_live_YOUR_KEY',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const result = await verifier.verify(token);
|
|
21
|
+
// { valid: true, agentId, name, issuer, scopes, expiresAt }
|
|
22
|
+
// { valid: false, error: 'TOKEN_REVOKED' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID' }
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Express middleware
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
const { middleware } = createVerifier({ publishableKey: 'pk_live_...' });
|
|
29
|
+
|
|
30
|
+
app.get('/api/data', middleware(), (req, res) => {
|
|
31
|
+
const { agentId, scopes } = req.agent;
|
|
32
|
+
res.json({ message: `Hello, agent ${agentId}` });
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Offline mode
|
|
37
|
+
|
|
38
|
+
```javascript
|
|
39
|
+
const verifier = createVerifier({
|
|
40
|
+
publishableKey: 'pk_live_...',
|
|
41
|
+
mode: 'offline',
|
|
42
|
+
cacheTTL: 300,
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Docs
|
|
47
|
+
|
|
48
|
+
[docs.imarobot.ai/sdks/verify](https://docs.imarobot.ai/sdks/verify)
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
MIT — [Humans and Robots LLC](https://humansandrobots.ai)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface VerifierOptions {
|
|
2
|
+
/** Your pk_live_ or pk_test_ publishable key */
|
|
3
|
+
publishableKey: string;
|
|
4
|
+
/** 'online' (default) checks revocation; 'offline' is local-only */
|
|
5
|
+
mode?: 'online' | 'offline';
|
|
6
|
+
/** Seconds to cache public keys (default: 300) */
|
|
7
|
+
cacheTTL?: number;
|
|
8
|
+
/** Ms before falling back to offline (default: 3000) */
|
|
9
|
+
timeout?: number;
|
|
10
|
+
/** API base URL (default: https://api.imarobot.ai) */
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface VerifyResultValid {
|
|
14
|
+
valid: true;
|
|
15
|
+
agentId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
issuer: string;
|
|
18
|
+
scopes: string[];
|
|
19
|
+
description?: string;
|
|
20
|
+
issuedAt?: string;
|
|
21
|
+
expiresAt: string;
|
|
22
|
+
verifiedAt?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface VerifyResultInvalid {
|
|
25
|
+
valid: false;
|
|
26
|
+
error: 'TOKEN_REVOKED' | 'TOKEN_EXPIRED' | 'TOKEN_INVALID';
|
|
27
|
+
}
|
|
28
|
+
export type VerifyResult = VerifyResultValid | VerifyResultInvalid;
|
|
29
|
+
export interface AgentInfo {
|
|
30
|
+
agentId: string;
|
|
31
|
+
name: string;
|
|
32
|
+
issuer: string;
|
|
33
|
+
scopes: string[];
|
|
34
|
+
expiresAt: string;
|
|
35
|
+
}
|
|
36
|
+
declare global {
|
|
37
|
+
namespace Express {
|
|
38
|
+
interface Request {
|
|
39
|
+
agent?: AgentInfo;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export declare function createVerifier(options: VerifierOptions): {
|
|
44
|
+
verify: (token: string) => Promise<VerifyResult>;
|
|
45
|
+
middleware: () => (req: any, res: any, next: any) => Promise<any>;
|
|
46
|
+
warmCache: (domains?: string[]) => Promise<void>;
|
|
47
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.createVerifier = createVerifier;
|
|
37
|
+
const crypto = __importStar(require("crypto"));
|
|
38
|
+
const https = __importStar(require("https"));
|
|
39
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
40
|
+
function httpGet(url, timeoutMs) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const req = https.get(url, { timeout: timeoutMs }, (res) => {
|
|
43
|
+
let data = '';
|
|
44
|
+
res.on('data', (chunk) => (data += chunk));
|
|
45
|
+
res.on('end', () => {
|
|
46
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
47
|
+
resolve(data);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
req.on('error', reject);
|
|
55
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timed out')); });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function base64UrlDecode(str) {
|
|
59
|
+
const base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
60
|
+
const padding = '='.repeat((4 - (base64.length % 4)) % 4);
|
|
61
|
+
return Buffer.from(base64 + padding, 'base64').toString('utf8');
|
|
62
|
+
}
|
|
63
|
+
function decodeJwtPayload(token) {
|
|
64
|
+
const parts = token.split('.');
|
|
65
|
+
if (parts.length !== 3)
|
|
66
|
+
throw new Error('Invalid JWT format');
|
|
67
|
+
return JSON.parse(base64UrlDecode(parts[1]));
|
|
68
|
+
}
|
|
69
|
+
// ─── Verifier ────────────────────────────────────────────────────────────────
|
|
70
|
+
function createVerifier(options) {
|
|
71
|
+
const { publishableKey, mode = 'online', cacheTTL = 300, timeout = 3000, baseUrl = 'https://api.imarobot.ai', } = options;
|
|
72
|
+
if (!publishableKey)
|
|
73
|
+
throw new Error('publishableKey is required');
|
|
74
|
+
// Public key cache: domain → { key, fetchedAt }
|
|
75
|
+
const keyCache = new Map();
|
|
76
|
+
async function fetchPublicKey(domain) {
|
|
77
|
+
const cacheKey = domain || '__default__';
|
|
78
|
+
const cached = keyCache.get(cacheKey);
|
|
79
|
+
if (cached && (Date.now() - cached.fetchedAt) / 1000 < cacheTTL) {
|
|
80
|
+
return cached.key;
|
|
81
|
+
}
|
|
82
|
+
const url = `${baseUrl}/.well-known/public-key.pem`;
|
|
83
|
+
const pem = await httpGet(url, timeout);
|
|
84
|
+
keyCache.set(cacheKey, { key: pem.trim(), fetchedAt: Date.now() });
|
|
85
|
+
return pem.trim();
|
|
86
|
+
}
|
|
87
|
+
function verifyOffline(token, publicKeyPem) {
|
|
88
|
+
try {
|
|
89
|
+
const payload = decodeJwtPayload(token);
|
|
90
|
+
// Check expiry
|
|
91
|
+
if (payload.exp && payload.exp * 1000 < Date.now()) {
|
|
92
|
+
return { valid: false, error: 'TOKEN_EXPIRED' };
|
|
93
|
+
}
|
|
94
|
+
// Verify RS256 signature
|
|
95
|
+
const [headerB64, payloadB64, signatureB64] = token.split('.');
|
|
96
|
+
const signedContent = `${headerB64}.${payloadB64}`;
|
|
97
|
+
const signature = Buffer.from(signatureB64.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (signatureB64.length % 4)) % 4), 'base64');
|
|
98
|
+
const verifier = crypto.createVerify('RSA-SHA256');
|
|
99
|
+
verifier.update(signedContent);
|
|
100
|
+
if (!verifier.verify(publicKeyPem, signature)) {
|
|
101
|
+
return { valid: false, error: 'TOKEN_INVALID' };
|
|
102
|
+
}
|
|
103
|
+
const agent = payload.agent || {};
|
|
104
|
+
return {
|
|
105
|
+
valid: true,
|
|
106
|
+
agentId: payload.sub,
|
|
107
|
+
name: agent.name || '',
|
|
108
|
+
issuer: payload.iss || '',
|
|
109
|
+
scopes: agent.scopes || [],
|
|
110
|
+
description: agent.description,
|
|
111
|
+
issuedAt: payload.iat ? new Date(payload.iat * 1000).toISOString() : undefined,
|
|
112
|
+
expiresAt: payload.exp ? new Date(payload.exp * 1000).toISOString() : '',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return { valid: false, error: 'TOKEN_INVALID' };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Verify an ImaRobot agent token.
|
|
121
|
+
*/
|
|
122
|
+
async function verify(token) {
|
|
123
|
+
if (!token || typeof token !== 'string') {
|
|
124
|
+
return { valid: false, error: 'TOKEN_INVALID' };
|
|
125
|
+
}
|
|
126
|
+
// Online mode: call the API
|
|
127
|
+
if (mode === 'online') {
|
|
128
|
+
try {
|
|
129
|
+
const url = `${baseUrl}/v1/verify/${encodeURIComponent(token)}`;
|
|
130
|
+
const body = await httpGet(url, timeout);
|
|
131
|
+
const data = JSON.parse(body);
|
|
132
|
+
if (data.valid) {
|
|
133
|
+
return {
|
|
134
|
+
valid: true,
|
|
135
|
+
agentId: data.agent_id,
|
|
136
|
+
name: data.name || '',
|
|
137
|
+
issuer: data.issuer || '',
|
|
138
|
+
scopes: data.scopes || [],
|
|
139
|
+
description: data.description,
|
|
140
|
+
issuedAt: data.issued_at,
|
|
141
|
+
expiresAt: data.expires_at || '',
|
|
142
|
+
verifiedAt: data.verified_at,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const errorMap = {
|
|
146
|
+
TOKEN_REVOKED: 'TOKEN_REVOKED',
|
|
147
|
+
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
|
|
148
|
+
};
|
|
149
|
+
return { valid: false, error: errorMap[data.error] || 'TOKEN_INVALID' };
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Fallback to offline on network error
|
|
153
|
+
try {
|
|
154
|
+
const publicKey = await fetchPublicKey();
|
|
155
|
+
return verifyOffline(token, publicKey);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return { valid: false, error: 'TOKEN_INVALID' };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Offline mode: local validation only
|
|
163
|
+
try {
|
|
164
|
+
const publicKey = await fetchPublicKey();
|
|
165
|
+
return verifyOffline(token, publicKey);
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return { valid: false, error: 'TOKEN_INVALID' };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Express middleware. Attaches req.agent on success, returns 401 on failure.
|
|
173
|
+
*/
|
|
174
|
+
function middleware() {
|
|
175
|
+
return async (req, res, next) => {
|
|
176
|
+
const authHeader = req.headers?.['authorization'] || '';
|
|
177
|
+
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : null;
|
|
178
|
+
if (!token) {
|
|
179
|
+
return res.status(401).json({
|
|
180
|
+
error: { code: 'UNAUTHORIZED', message: 'Missing Authorization header.' },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
const result = await verify(token);
|
|
184
|
+
if (!result.valid) {
|
|
185
|
+
return res.status(401).json({
|
|
186
|
+
error: { code: 'UNAUTHORIZED', message: result.error },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
req.agent = {
|
|
190
|
+
agentId: result.agentId,
|
|
191
|
+
name: result.name,
|
|
192
|
+
issuer: result.issuer,
|
|
193
|
+
scopes: result.scopes,
|
|
194
|
+
expiresAt: result.expiresAt,
|
|
195
|
+
};
|
|
196
|
+
next();
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Pre-fetch and cache public keys for known issuer domains.
|
|
201
|
+
*/
|
|
202
|
+
async function warmCache(domains) {
|
|
203
|
+
await fetchPublicKey();
|
|
204
|
+
}
|
|
205
|
+
return { verify, middleware, warmCache };
|
|
206
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "imarobot-verify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Verify ImaRobot agent identity tokens. Open source receiver-side SDK.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc",
|
|
10
|
+
"prepublishOnly": "npm run build"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["imarobot", "agent", "identity", "verification", "jwt", "ai", "agent-identity"],
|
|
13
|
+
"author": "Humans and Robots LLC <rafi@humansandrobots.ai>",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/humansandrobots/imarobot-verify"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://docs.imarobot.ai/sdks/verify",
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^5.4.0",
|
|
25
|
+
"@types/node": "^20.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|