@imagxp/protocol 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,385 @@
1
+ /**
2
+ * Layer 2: Publisher Middleware
3
+ * Used by content owners to enforce policy, log access, and filter bots.
4
+ */
5
+ import { HEADERS, MAX_CLOCK_SKEW_MS, WELL_KNOWN_AGENT_PATH } from './constants.js';
6
+ import { exportPublicKey, signData, verifySignature } from './crypto.js';
7
+ import { AccessPurpose } from './types.js';
8
+ import { verifyJwt } from './proof.js';
9
+ import { jwtVerify, createRemoteJWKSet } from 'jose';
10
+ /**
11
+ * Default In-Memory Cache (Fallback only)
12
+ * NOT recommended for high-traffic Serverless production.
13
+ */
14
+ class MemoryCache {
15
+ constructor() {
16
+ this.store = new Map();
17
+ }
18
+ async get(key) {
19
+ const item = this.store.get(key);
20
+ if (!item)
21
+ return null;
22
+ if (Date.now() > item.exp) {
23
+ this.store.delete(key);
24
+ return null;
25
+ }
26
+ return item.val;
27
+ }
28
+ async set(key, value, ttlSeconds) {
29
+ this.store.set(key, {
30
+ val: value,
31
+ exp: Date.now() + (ttlSeconds * 1000)
32
+ });
33
+ }
34
+ }
35
+ export class IMAGXPPublisher {
36
+ constructor(policy, strategy = 'PASSIVE', cacheImpl) {
37
+ this.keyPair = null;
38
+ // Default TTL: 1 Hour
39
+ this.CACHE_TTL_SECONDS = 3600;
40
+ this.policy = policy;
41
+ this.unauthenticatedStrategy = strategy;
42
+ this.cache = cacheImpl || new MemoryCache();
43
+ }
44
+ async initialize(keyPair) {
45
+ this.keyPair = keyPair;
46
+ }
47
+ getPolicy() {
48
+ return this.policy;
49
+ }
50
+ /**
51
+ * Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
52
+ * STAGE 1: IDENTITY (Strict)
53
+ * STAGE 2: POLICY (Permissions)
54
+ * STAGE 3: ACCESS (HQ Content)
55
+ */
56
+ async evaluateVisitor(reqHeaders, rawPayload) {
57
+ console.log(`\n--- [IMAGXP LOG START] New Request ---`);
58
+ // --- STAGE 1: IDENTITY VERIFICATION ---
59
+ console.log(`[IDENTITY] 🔍 Checking Identity Headers...`);
60
+ const hasImagxp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
61
+ if (hasImagxp) {
62
+ // It claims to be an Agent. Verify it STRICTLY.
63
+ return await this.handleAgentStrict(reqHeaders, rawPayload);
64
+ }
65
+ // If NO IMAGXP Headers -> FAIL IDENTITY immediately.
66
+ console.log(`[IDENTITY] ❌ FAILED. No IMAGXP Headers found.`);
67
+ // For now, retaining the legacy "Passive/Hybrid" switch just to avoid breaking browser demos completely
68
+ // BUT logging it as a specific "Identity Fail" flow.
69
+ if (this.unauthenticatedStrategy === 'STRICT') {
70
+ console.log(`[IDENTITY] ⛔ BLOCKING. Strategy is STRICT.`);
71
+ return {
72
+ allowed: false,
73
+ status: 401,
74
+ reason: "IDENTITY_REQUIRED: Missing IMAGXP Headers.",
75
+ visitorType: 'UNIDENTIFIED_BOT'
76
+ };
77
+ }
78
+ console.log(`[IDENTITY] ⚠️ SKIPPED (Legacy Mode). Checking Browser Heuristics...`);
79
+ const isHuman = this.performBrowserHeuristics(reqHeaders);
80
+ if (isHuman) {
81
+ console.log(`[POLICY] 👤 ALLOWED. Browser Heuristics Passed.`);
82
+ return { allowed: true, status: 200, reason: "BROWSER_VERIFIED", visitorType: 'LIKELY_HUMAN' };
83
+ }
84
+ console.log(`[IDENTITY] ❌ FAILED. Not a Browser, No Headers.`);
85
+ console.log(`[ACCESS] ⛔ BLOCKED.`);
86
+ return {
87
+ allowed: false,
88
+ status: 403,
89
+ reason: "IDENTITY_FAIL: No Identity, No Browser.",
90
+ visitorType: 'UNIDENTIFIED_BOT'
91
+ };
92
+ }
93
+ /**
94
+ * Browser Heuristics (Hardened)
95
+ * 1. Checks Known Bot Signatures (Fast Fail)
96
+ * 2. Checks Trusted Upstream Signals (Cloudflare/Vercel)
97
+ * 3. Checks Browser Header Consistency
98
+ */
99
+ performBrowserHeuristics(headers) {
100
+ const userAgent = headers['user-agent'] || '';
101
+ // A. The "Obvious Bot" Blocklist (Fast Fail)
102
+ const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
103
+ if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
104
+ return false;
105
+ }
106
+ // B. Trusted Infrastructure Signals (The Real World Solution)
107
+ if (headers['cf-visitor'] || headers['cf-ray'])
108
+ return true;
109
+ if (headers['x-vercel-id'])
110
+ return true;
111
+ if (headers['cloudfront-viewer-address'])
112
+ return true;
113
+ // C. The "Browser Fingerprint" (Fallback for direct connections)
114
+ const hasAcceptLanguage = !!headers['accept-language'];
115
+ const hasSecFetchDest = !!headers['sec-fetch-dest'];
116
+ const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
117
+ if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
118
+ return true;
119
+ }
120
+ return false;
121
+ }
122
+ /**
123
+ * Handle IMAGXP Protocol Logic (Strict Mode)
124
+ */
125
+ async handleAgentStrict(reqHeaders, rawPayload) {
126
+ let agentId = "UNKNOWN";
127
+ try {
128
+ // 1. Decode Headers
129
+ const payloadHeader = reqHeaders[HEADERS.PAYLOAD];
130
+ const sigHeader = reqHeaders[HEADERS.SIGNATURE];
131
+ const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY];
132
+ const headerJson = atob(payloadHeader);
133
+ const requestHeader = JSON.parse(headerJson);
134
+ agentId = requestHeader.agent_id;
135
+ console.log(`[IDENTITY] 🆔 Claimed ID: ${agentId}`);
136
+ // 2. Crypto & DNS Verification
137
+ const signedRequest = {
138
+ header: requestHeader,
139
+ signature: sigHeader,
140
+ publicKey: keyHeader
141
+ };
142
+ const agentKey = await crypto.subtle.importKey("spki", new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))), { name: "ECDSA", namedCurve: "P-256" }, true, ["verify"]);
143
+ // Verify Core Logic (DNS + Crypto)
144
+ const verification = await this.verifyRequestLogic(signedRequest, agentKey);
145
+ if (!verification.identityVerified) {
146
+ console.log(`[IDENTITY] ❌ FAILED. Reason: ${verification.reason}`);
147
+ console.log(`[ACCESS] ⛔ BLOCKED.`);
148
+ return { allowed: false, status: 403, reason: verification.reason, visitorType: 'UNIDENTIFIED_BOT' };
149
+ }
150
+ console.log(`[IDENTITY] ✅ PASSED. DNS Binding Verified.`);
151
+ // --- STAGE 2: POLICY ENFORCEMENT ---
152
+ console.log(`[POLICY] 📜 Checking Permissions for ${agentId}...`);
153
+ const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
154
+ const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
155
+ const policyResult = await this.checkPolicyStrict(requestHeader, proofToken, paymentCredential);
156
+ if (!policyResult.allowed) {
157
+ console.log(`[POLICY] ⛔ DENIED. Reason: ${policyResult.reason}`);
158
+ console.log(`[ACCESS] ⛔ BLOCKED.`);
159
+ return policyResult;
160
+ }
161
+ // --- STAGE 3: ACCESS GRANT ---
162
+ console.log(`[POLICY] ✅ PASSED. Requirements Met.`);
163
+ console.log(`[ACCESS] 🔓 GRANTED. Unlocking HQ Content.`);
164
+ return {
165
+ allowed: true,
166
+ status: 200,
167
+ reason: "IMAGXP_VERIFIED",
168
+ visitorType: 'VERIFIED_AGENT',
169
+ metadata: requestHeader,
170
+ proofUsed: policyResult.proofUsed
171
+ };
172
+ }
173
+ catch (e) {
174
+ console.error(`[IMAGXP ERROR]`, e);
175
+ return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
176
+ }
177
+ }
178
+ // Legacy handler kept for interface compatibility (deprecated)
179
+ async handleAgent(reqHeaders, rawPayload) {
180
+ return this.handleAgentStrict(reqHeaders, rawPayload);
181
+ }
182
+ /**
183
+ * STAGE 2: POLICY ENFORCEMENT CHECK
184
+ */
185
+ async checkPolicyStrict(requestHeader, proofToken, paymentCredential) {
186
+ // 1. Policy Check: Purpose Ban (e.g. No Training)
187
+ if (requestHeader.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
188
+ return { allowed: false, status: 403, reason: 'POLICY_DENIED: Training not allowed.', visitorType: 'VERIFIED_AGENT' };
189
+ }
190
+ // 2. BROKER CHECK (New v1.1)
191
+ if (this.policy.monetization?.brokerUrl) {
192
+ const brokerUrl = this.policy.monetization.brokerUrl;
193
+ if (!paymentCredential) {
194
+ return { allowed: false, status: 402, reason: "PAYMENT_REQUIRED: Missing Broker Credential", visitorType: 'UNIDENTIFIED_BOT' };
195
+ }
196
+ const isValid = await this.verifyBrokerCred(paymentCredential, brokerUrl);
197
+ if (!isValid) {
198
+ return { allowed: false, status: 403, reason: "PAYMENT_DENIED: Invalid Broker Token", visitorType: 'UNIDENTIFIED_BOT' };
199
+ }
200
+ // If valid, we record the "Proof Used" so we can settle later
201
+ return { allowed: true, status: 200, reason: "IMAGXP_PAID", visitorType: "VERIFIED_AGENT", proofUsed: `BROKER_JWT:${paymentCredential.slice(0, 10)}...` };
202
+ }
203
+ if (requestHeader.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
204
+ return { allowed: false, status: 403, reason: 'POLICY_DENIED: RAG not allowed.', visitorType: 'VERIFIED_AGENT' };
205
+ }
206
+ // 2. Policy Check: Economics (v1.2) - Payment & Ads
207
+ if (this.policy.requiresPayment) {
208
+ let paymentSatisfied = false;
209
+ // Method A: Flexible Payment Callback (DB / Custom Logic)
210
+ if (this.policy.monetization?.checkPayment) {
211
+ const isPaid = await this.policy.monetization.checkPayment(requestHeader.agent_id, requestHeader.purpose);
212
+ if (isPaid) {
213
+ console.log(`[POLICY] 💰 Payment Verified via Callback.`);
214
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'WHITELIST_CALLBACK' };
215
+ }
216
+ }
217
+ // Method B: Payment Credentials (Unified JWT)
218
+ if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
219
+ const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
220
+ console.log(`[POLICY] 🔐 Verifying Payment Credential (Issuer: ${issuer})...`);
221
+ const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
222
+ if (isValidCredential) {
223
+ console.log(`[POLICY] ✅ Credential Signature VALID.`);
224
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
225
+ }
226
+ else {
227
+ console.log(`[POLICY] ❌ Credential Signature INVALID.`);
228
+ }
229
+ }
230
+ // Method C: Ad-Supported (Proof Verification)
231
+ if (!paymentSatisfied && this.policy.allowAdSupportedAccess && requestHeader.context?.ads_displayed) {
232
+ if (proofToken && this.policy.monetization?.adNetwork) {
233
+ const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
234
+ console.log(`[POLICY] 📺 Verifying Ad Proof (Issuer: ${issuer})...`);
235
+ const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
236
+ if (isValidProof) {
237
+ console.log(`[POLICY] ✅ Ad Proof Signature VALID.`);
238
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'AD_PROOF_JWT' };
239
+ }
240
+ else {
241
+ console.log(`[POLICY] ❌ Ad Proof Signature INVALID.`);
242
+ }
243
+ }
244
+ else {
245
+ console.log(`[POLICY] ⚠️ Ad Proof MISSING.`);
246
+ }
247
+ }
248
+ return {
249
+ allowed: false,
250
+ status: 402,
251
+ reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.',
252
+ visitorType: 'VERIFIED_AGENT',
253
+ proofUsed: 'NONE'
254
+ };
255
+ }
256
+ // If no payment required, allow.
257
+ return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT' };
258
+ }
259
+ async verifyRequestLogic(request, requestPublicKey) {
260
+ // 1. Replay Attack Prevention
261
+ const requestTime = new Date(request.header.ts).getTime();
262
+ if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
263
+ return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew too large.', identityVerified: false };
264
+ }
265
+ // 2. Crypto Verification
266
+ const signableString = JSON.stringify(request.header);
267
+ const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
268
+ if (!isCryptoValid)
269
+ return { allowed: false, reason: 'CRYPTO_FAIL: Signature invalid.', identityVerified: false };
270
+ // 3. Identity Verification (DNS Binding) with Cache
271
+ let identityVerified = false;
272
+ const claimedDomain = request.header.agent_id;
273
+ const pubKeyString = await exportPublicKey(requestPublicKey);
274
+ console.log(`[IDENTITY] 🔍 Verifying DNS Binding for: ${claimedDomain}`);
275
+ // Check Cache First
276
+ const cachedKey = await this.cache.get(claimedDomain);
277
+ if (cachedKey === pubKeyString) {
278
+ console.log("[IDENTITY] ⚡ Cache Hit. Identity Verified.");
279
+ identityVerified = true;
280
+ }
281
+ else if (this.isDomain(claimedDomain)) {
282
+ // Cache Miss: Perform DNS Fetch
283
+ identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
284
+ if (identityVerified) {
285
+ await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
286
+ }
287
+ }
288
+ if (this.policy.requireIdentityBinding && !identityVerified) {
289
+ return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
290
+ }
291
+ // Return verified status so handleAgentStrict can proceed to Policy Check
292
+ return { allowed: true, reason: 'OK', identityVerified: identityVerified };
293
+ }
294
+ async verifyDnsBinding(domain, requestKeySpki) {
295
+ try {
296
+ // Allow HTTP for localhost testing
297
+ const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
298
+ const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
299
+ console.log(` 🌍 [IMAGXP DNS] Fetching Manifest: ${url} ...`);
300
+ // In production, we need a short timeout to prevent hanging
301
+ const controller = new AbortController();
302
+ const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
303
+ const response = await fetch(url, { signal: controller.signal });
304
+ clearTimeout(timeoutId);
305
+ if (!response.ok) {
306
+ console.log(` ❌ [IMAGXP DNS] Fetch Failed: ${response.status}`);
307
+ return false;
308
+ }
309
+ const manifest = await response.json();
310
+ console.log(` 📄 [IMAGXP DNS] Manifest received. Agent ID: ${manifest.agent_id}`);
311
+ // CHECK 1: Does the manifest actually belong to the domain?
312
+ if (manifest.agent_id !== domain) {
313
+ console.log(` ❌ [IMAGXP DNS] Mismatch: Manifest ID ${manifest.agent_id} != Claimed ${domain}`);
314
+ return false;
315
+ }
316
+ // CHECK 2: Does the key match?
317
+ if (manifest.public_key !== requestKeySpki) {
318
+ console.log(` ❌ [IMAGXP DNS] Key Mismatch: DNS Key != Request Key`);
319
+ return false;
320
+ }
321
+ console.log(` ✅ [IMAGXP DNS] Identity Confirmed.`);
322
+ return true;
323
+ }
324
+ catch (e) {
325
+ console.log(` ❌ [IMAGXP DNS] Error: ${e.message}`);
326
+ return false;
327
+ }
328
+ }
329
+ /**
330
+ * NEW: Verify a Broker-Issued Token (JWT)
331
+ * Checks if the request contains a valid "Visa" from the Broker.
332
+ */
333
+ async verifyBrokerCred(credential, brokerUrl) {
334
+ try {
335
+ // 1. Fetch Broker's Public Keys (JWKS)
336
+ const JWKS = createRemoteJWKSet(new URL(`${brokerUrl}/.well-known/jwks.json`));
337
+ // 2. Verify the Token Signature
338
+ const { payload } = await jwtVerify(credential, JWKS, {
339
+ issuer: brokerUrl, // Ensure it came from THE Broker
340
+ clockTolerance: 5 // Allow 5s clock skew
341
+ });
342
+ console.log(`[BROKER] 💰 Valid Payment Token from ${payload.iss} for amount ${payload.amount}`);
343
+ return true;
344
+ }
345
+ catch (e) {
346
+ console.warn(`[BROKER] ❌ Invalid Token:`, e.message);
347
+ return false;
348
+ }
349
+ }
350
+ isDomain(s) {
351
+ // Basic regex, allows localhost with ports
352
+ return /^[a-zA-Z0-9.-]+(:\d+)?$/.test(s) || /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(s);
353
+ }
354
+ async generateResponseHeaders(origin) {
355
+ if (!this.keyPair)
356
+ throw new Error("Publisher keys not initialized");
357
+ const payload = JSON.stringify({ origin, ts: Date.now() });
358
+ const signature = await signData(this.keyPair.privateKey, payload);
359
+ return {
360
+ [HEADERS.CONTENT_ORIGIN]: origin,
361
+ [HEADERS.PROVENANCE_SIG]: signature
362
+ };
363
+ }
364
+ /**
365
+ * Handling Quality Feedback (The "Dispute" Layer)
366
+ * This runs when an Agent sends 'x-imagxp-feedback'.
367
+ */
368
+ async handleFeedback(token, headers) {
369
+ // NOTE: In production, you would fetch the Agent's specific key.
370
+ // For now, we assume standard Discovery or a centralized Key Set (like adNetwork).
371
+ // Ideally, the SDK config should have a 'qualityOracle' key set.
372
+ // 1. We just Decode it to Log it (Verification is optional but recommended)
373
+ try {
374
+ const parts = token.split('.');
375
+ const payload = JSON.parse(atob(parts[1]));
376
+ console.log(`\n📢 [IMAGXP QUALITY ALERT] Feedback Received from ${payload.agent_id}`);
377
+ console.log(` Reason: ${payload.reason} | Score: ${payload.quality_score}`);
378
+ console.log(` Resource: ${payload.url}`);
379
+ console.log(` (Signature available for dispute evidence)`);
380
+ }
381
+ catch (e) {
382
+ console.log(` ⚠️ [IMAGXP Warning] Malformed Feedback Token.`);
383
+ }
384
+ }
385
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Layer 1: Protocol Definitions
3
+ * Shared types used by both Agent and Publisher.
4
+ */
5
+ export declare enum AccessPurpose {
6
+ CRAWL_TRAINING = "CRAWL_TRAINING",
7
+ RAG_RETRIEVAL = "RAG_RETRIEVAL",
8
+ SUMMARY = "SUMMARY",
9
+ QUOTATION = "QUOTATION",
10
+ EMBEDDING = "EMBEDDING"
11
+ }
12
+ export declare enum ContentOrigin {
13
+ HUMAN = "HUMAN",// Created by humans. High training value.
14
+ SYNTHETIC = "SYNTHETIC",// Created by AI. Risk of model collapse.
15
+ HYBRID = "HYBRID"
16
+ }
17
+ export declare enum QualityFlag {
18
+ SEO_SPAM = "SEO_SPAM",
19
+ INACCURATE = "INACCURATE",
20
+ HATE_SPEECH = "HATE_SPEECH",
21
+ HIGH_QUALITY = "HIGH_QUALITY"
22
+ }
23
+ /**
24
+ * DNS Identity Manifest
25
+ * Hosted at: https://{agent_id}/.well-known/imagxp-agent.json
26
+ */
27
+ export interface AgentIdentityManifest {
28
+ agent_id: string;
29
+ public_key: string;
30
+ contact_email?: string;
31
+ }
32
+ /**
33
+ * PRODUCTION INFRASTRUCTURE: Cache Interface
34
+ * Required for Serverless/Edge environments to prevent repeated DNS fetches.
35
+ */
36
+ export interface IdentityCache {
37
+ get(key: string): Promise<string | null>;
38
+ set(key: string, value: string, ttlSeconds: number): Promise<void>;
39
+ }
40
+ /**
41
+ * Optional Monetization (The Settlement Layer)
42
+ */
43
+ /**
44
+ * Optional Monetization (The Settlement Layer)
45
+ */
46
+ export interface MonetizationConfig {
47
+ checkPayment?: (agentId: string, purpose: string) => boolean | Promise<boolean>;
48
+ adNetwork?: {
49
+ jwksUrl: string;
50
+ issuer: string;
51
+ };
52
+ brokerUrl?: string;
53
+ paymentConfig?: {
54
+ jwksUrl: string;
55
+ issuer: string;
56
+ };
57
+ }
58
+ /**
59
+ * Handling Non-IMAGXP Visitors
60
+ *
61
+ * PASSIVE: Allow everyone (Legacy web behavior).
62
+ * HYBRID: Allow verified Agents AND likely Humans (Browser Heuristics). Block bots.
63
+ * STRICT: Allow ONLY verified IMAGXP Agents. (API Mode).
64
+ */
65
+ export type UnauthenticatedStrategy = 'PASSIVE' | 'HYBRID' | 'STRICT';
66
+ export interface AccessPolicy {
67
+ version: '1.1';
68
+ allowTraining: boolean;
69
+ allowRAG: boolean;
70
+ attributionRequired: boolean;
71
+ allowAdSupportedAccess: boolean;
72
+ requiresPayment: boolean;
73
+ paymentPointer?: string;
74
+ requireIdentityBinding?: boolean;
75
+ monetization?: MonetizationConfig;
76
+ }
77
+ export interface ProtocolHeader {
78
+ v: '1.1';
79
+ ts: string;
80
+ agent_id: string;
81
+ resource: string;
82
+ purpose: AccessPurpose;
83
+ context: {
84
+ ads_displayed: boolean;
85
+ };
86
+ }
87
+ export interface SignedAccessRequest {
88
+ header: ProtocolHeader;
89
+ signature: string;
90
+ publicKey?: string;
91
+ }
92
+ export interface FeedbackSignal {
93
+ target_resource: string;
94
+ agent_id: string;
95
+ quality_score: number;
96
+ flags: QualityFlag[];
97
+ timestamp: string;
98
+ }
99
+ export interface EvaluationResult {
100
+ allowed: boolean;
101
+ status: 200 | 400 | 401 | 402 | 403;
102
+ reason: string;
103
+ visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
104
+ metadata?: any;
105
+ payment_status?: 'PAID_SUBSCRIBER' | 'AD_FUNDED' | 'UNPAID';
106
+ proofUsed?: string;
107
+ }
108
+ export interface FeedbackSignalToken {
109
+ url: string;
110
+ agent_id: string;
111
+ quality_score: number;
112
+ reason: string;
113
+ timestamp: number;
114
+ }
package/dist/types.js ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Layer 1: Protocol Definitions
3
+ * Shared types used by both Agent and Publisher.
4
+ */
5
+ export var AccessPurpose;
6
+ (function (AccessPurpose) {
7
+ AccessPurpose["CRAWL_TRAINING"] = "CRAWL_TRAINING";
8
+ AccessPurpose["RAG_RETRIEVAL"] = "RAG_RETRIEVAL";
9
+ AccessPurpose["SUMMARY"] = "SUMMARY";
10
+ AccessPurpose["QUOTATION"] = "QUOTATION";
11
+ AccessPurpose["EMBEDDING"] = "EMBEDDING";
12
+ })(AccessPurpose || (AccessPurpose = {}));
13
+ export var ContentOrigin;
14
+ (function (ContentOrigin) {
15
+ ContentOrigin["HUMAN"] = "HUMAN";
16
+ ContentOrigin["SYNTHETIC"] = "SYNTHETIC";
17
+ ContentOrigin["HYBRID"] = "HYBRID"; // Edited by humans, drafted by AI.
18
+ })(ContentOrigin || (ContentOrigin = {}));
19
+ export var QualityFlag;
20
+ (function (QualityFlag) {
21
+ QualityFlag["SEO_SPAM"] = "SEO_SPAM";
22
+ QualityFlag["INACCURATE"] = "INACCURATE";
23
+ QualityFlag["HATE_SPEECH"] = "HATE_SPEECH";
24
+ QualityFlag["HIGH_QUALITY"] = "HIGH_QUALITY";
25
+ })(QualityFlag || (QualityFlag = {}));
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@imagxp/protocol",
3
+ "version": "1.0.0",
4
+ "description": "TypeScript reference implementation of IMAGXP v1.1",
5
+ "keywords": [
6
+ "imagxp",
7
+ "aamp",
8
+ "protocol",
9
+ "agent",
10
+ "ai",
11
+ "monetization",
12
+ "content negotiation",
13
+ "publisher",
14
+ "middleware",
15
+ "robots.txt",
16
+ "crawler",
17
+ "llm",
18
+ "rag",
19
+ "seo",
20
+ "verification"
21
+ ],
22
+ "main": "dist/index.js",
23
+ "types": "dist/index.d.ts",
24
+ "exports": {
25
+ ".": "./dist/index.js",
26
+ "./dist/nextjs": "./dist/nextjs.js",
27
+ "./dist/agent": "./dist/agent.js",
28
+ "./dist/types": "./dist/types.js"
29
+ },
30
+ "type": "module",
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "test": "node --loader ts-node/esm test/handshake.spec.ts"
34
+ },
35
+ "repository": {
36
+ "license": "Apache-2.0",
37
+ "type": "git",
38
+ "url": "https://github.com/imagxp-protocol/imagxp.git"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.0.0",
45
+ "@types/node-fetch": "^2.6.13",
46
+ "ts-node": "^10.9.0",
47
+ "typescript": "^5.0.0"
48
+ },
49
+ "dependencies": {
50
+ "jose": "^6.1.3",
51
+ "node-fetch": "^2.7.0"
52
+ }
53
+ }
package/src/agent.ts ADDED
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Layer 2: Agent SDK
3
+ */
4
+ import { AccessPurpose, ProtocolHeader, SignedAccessRequest, FeedbackSignal, QualityFlag, AgentIdentityManifest } from './types.js';
5
+ import { generateKeyPair, signData, exportPublicKey } from './crypto.js';
6
+ import { IMAGXP_VERSION } from './constants.js';
7
+
8
+ export interface AccessOptions {
9
+ adsDisplayed?: boolean;
10
+ }
11
+
12
+ export class IMAGXPAgent {
13
+ private keyPair: CryptoKeyPair | null = null;
14
+ public agentId: string = "pending";
15
+
16
+ /**
17
+ * Initialize the Agent Identity (Ephemeral or Persisted)
18
+ * @param customAgentId For PRODUCTION, this should be your domain (e.g., "bot.openai.com")
19
+ */
20
+ async initialize(customAgentId?: string) {
21
+ this.keyPair = await generateKeyPair();
22
+ // Use the provided ID (authentic) or generate a session ID (ephemeral)
23
+ this.agentId = customAgentId || "agent_" + Math.random().toString(36).substring(7);
24
+ }
25
+
26
+ async createAccessRequest(
27
+ resource: string,
28
+ purpose: AccessPurpose,
29
+ options: AccessOptions = {}
30
+ ): Promise<SignedAccessRequest> {
31
+ if (!this.keyPair) throw new Error("Agent not initialized. Call initialize() first.");
32
+
33
+ const header: ProtocolHeader = {
34
+ v: IMAGXP_VERSION,
35
+ ts: new Date().toISOString(),
36
+ agent_id: this.agentId,
37
+ resource,
38
+ purpose,
39
+ context: {
40
+ ads_displayed: options.adsDisplayed || false
41
+ }
42
+ };
43
+
44
+ const signature = await signData(this.keyPair.privateKey, JSON.stringify(header));
45
+ const publicKeyExport = await exportPublicKey(this.keyPair.publicKey);
46
+
47
+ return { header, signature, publicKey: publicKeyExport };
48
+ }
49
+
50
+ /**
51
+ * Helper: Generate the JSON file you must host on your domain
52
+ * Host this at: https://{agentId}/.well-known/imagxp-agent.json
53
+ */
54
+ async getIdentityManifest(contactEmail?: string): Promise<AgentIdentityManifest> {
55
+ if (!this.keyPair) throw new Error("Agent not initialized.");
56
+
57
+ const publicKey = await exportPublicKey(this.keyPair.publicKey);
58
+
59
+ return {
60
+ agent_id: this.agentId,
61
+ public_key: publicKey,
62
+ contact_email: contactEmail
63
+ };
64
+ }
65
+
66
+ /**
67
+ * NEW IN V1.1: Quality Feedback Loop
68
+ * Allows the Agent to report spam or verify quality of a resource.
69
+ */
70
+ async generateFeedback(
71
+ resource: string,
72
+ score: number,
73
+ flags: QualityFlag[]
74
+ ): Promise<{ signal: FeedbackSignal, signature: string }> {
75
+ if (!this.keyPair) throw new Error("Agent not initialized.");
76
+
77
+ const signal: FeedbackSignal = {
78
+ target_resource: resource,
79
+ agent_id: this.agentId,
80
+ quality_score: Math.max(0, Math.min(1, score)),
81
+ flags,
82
+ timestamp: new Date().toISOString()
83
+ };
84
+
85
+ const signature = await signData(this.keyPair.privateKey, JSON.stringify(signal));
86
+ return { signal, signature };
87
+ }
88
+ }