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 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)
@@ -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
+ }