@imagxp/protocol 1.0.3 → 1.0.6
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/package.json +5 -1
- package/src/agent.ts +0 -106
- package/src/cli.ts +0 -26
- package/src/constants.ts +0 -48
- package/src/crypto.ts +0 -95
- package/src/express.ts +0 -108
- package/src/index.ts +0 -13
- package/src/nextjs.ts +0 -109
- package/src/proof.ts +0 -36
- package/src/publisher.ts +0 -482
- package/src/types.ts +0 -150
- package/test/handshake.spec.ts +0 -63
- package/tsconfig.json +0 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imagxp/protocol",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "TypeScript reference implementation of IMAGXP v1.1",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"imagxp",
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public"
|
|
46
46
|
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md"
|
|
50
|
+
],
|
|
47
51
|
"devDependencies": {
|
|
48
52
|
"@types/node": "^20.0.0",
|
|
49
53
|
"@types/node-fetch": "^2.6.13",
|
package/src/agent.ts
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 2: Agent SDK
|
|
3
|
-
*/
|
|
4
|
-
import { AccessPurpose, ProtocolHeader, SignedAccessRequest, FeedbackSignal, QualityFlag, AgentIdentityManifest } from './types.js';
|
|
5
|
-
import { generateKeyPair, signData, exportPublicKey, importPrivateKey, importPublicKey } from './crypto.js';
|
|
6
|
-
import { IMAGXP_VERSION } from './constants.js';
|
|
7
|
-
|
|
8
|
-
declare const process: any;
|
|
9
|
-
|
|
10
|
-
export interface AccessOptions {
|
|
11
|
-
adsDisplayed?: boolean;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class IMAGXPAgent {
|
|
15
|
-
private keyPair: CryptoKeyPair | null = null;
|
|
16
|
-
public agentId: string = "pending";
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Initialize the Agent Identity (Ephemeral or Persisted)
|
|
20
|
-
* @param customAgentId For PRODUCTION, this should be your domain (e.g., "bot.openai.com")
|
|
21
|
-
*/
|
|
22
|
-
async initialize(customAgentId?: string) {
|
|
23
|
-
if (process.env.IMAGXP_PRIVATE_KEY && process.env.IMAGXP_PUBLIC_KEY) {
|
|
24
|
-
// 1. Load Persistent Identity from Environment
|
|
25
|
-
console.log(`[IMAGXP] Loading Agent Identity from Environment...`);
|
|
26
|
-
try {
|
|
27
|
-
const privateKey = await importPrivateKey(process.env.IMAGXP_PRIVATE_KEY);
|
|
28
|
-
const publicKey = await importPublicKey(process.env.IMAGXP_PUBLIC_KEY);
|
|
29
|
-
this.keyPair = { privateKey, publicKey };
|
|
30
|
-
} catch (e) {
|
|
31
|
-
console.error("Failed to load keys from env:", e);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Fallback or Normal Init
|
|
36
|
-
if (!this.keyPair) {
|
|
37
|
-
this.keyPair = await generateKeyPair();
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// Use the provided ID (authentic) or generate a session ID (ephemeral)
|
|
41
|
-
this.agentId = process.env.IMAGXP_AGENT_ID || customAgentId || "agent_" + Math.random().toString(36).substring(7);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async createAccessRequest(
|
|
45
|
-
resource: string,
|
|
46
|
-
purpose: AccessPurpose,
|
|
47
|
-
options: AccessOptions = {}
|
|
48
|
-
): Promise<SignedAccessRequest> {
|
|
49
|
-
if (!this.keyPair) throw new Error("Agent not initialized. Call initialize() first.");
|
|
50
|
-
|
|
51
|
-
const header: ProtocolHeader = {
|
|
52
|
-
v: IMAGXP_VERSION,
|
|
53
|
-
ts: new Date().toISOString(),
|
|
54
|
-
agent_id: this.agentId,
|
|
55
|
-
resource,
|
|
56
|
-
purpose,
|
|
57
|
-
context: {
|
|
58
|
-
ads_displayed: options.adsDisplayed || false
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
const signature = await signData(this.keyPair.privateKey, JSON.stringify(header));
|
|
63
|
-
const publicKeyExport = await exportPublicKey(this.keyPair.publicKey);
|
|
64
|
-
|
|
65
|
-
return { header, signature, publicKey: publicKeyExport };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Helper: Generate the JSON file you must host on your domain
|
|
70
|
-
* Host this at: https://{agentId}/.well-known/imagxp-agent.json
|
|
71
|
-
*/
|
|
72
|
-
async getIdentityManifest(contactEmail?: string): Promise<AgentIdentityManifest> {
|
|
73
|
-
if (!this.keyPair) throw new Error("Agent not initialized.");
|
|
74
|
-
|
|
75
|
-
const publicKey = await exportPublicKey(this.keyPair.publicKey);
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
agent_id: this.agentId,
|
|
79
|
-
public_key: publicKey,
|
|
80
|
-
contact_email: contactEmail
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* NEW IN V1.1: Quality Feedback Loop
|
|
86
|
-
* Allows the Agent to report spam or verify quality of a resource.
|
|
87
|
-
*/
|
|
88
|
-
async generateFeedback(
|
|
89
|
-
resource: string,
|
|
90
|
-
score: number,
|
|
91
|
-
flags: QualityFlag[]
|
|
92
|
-
): Promise<{ signal: FeedbackSignal, signature: string }> {
|
|
93
|
-
if (!this.keyPair) throw new Error("Agent not initialized.");
|
|
94
|
-
|
|
95
|
-
const signal: FeedbackSignal = {
|
|
96
|
-
target_resource: resource,
|
|
97
|
-
agent_id: this.agentId,
|
|
98
|
-
quality_score: Math.max(0, Math.min(1, score)),
|
|
99
|
-
flags,
|
|
100
|
-
timestamp: new Date().toISOString()
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const signature = await signData(this.keyPair.privateKey, JSON.stringify(signal));
|
|
104
|
-
return { signal, signature };
|
|
105
|
-
}
|
|
106
|
-
}
|
package/src/cli.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { generateKeyPair, exportPrivateKey, exportPublicKey } from './crypto.js';
|
|
3
|
-
declare const process: any;
|
|
4
|
-
|
|
5
|
-
async function main() {
|
|
6
|
-
const args = process.argv.slice(2);
|
|
7
|
-
const command = args[0];
|
|
8
|
-
|
|
9
|
-
if (command === 'generate-identity') {
|
|
10
|
-
console.log('Generating new IMAGXP Identity...');
|
|
11
|
-
const keyPair = await generateKeyPair();
|
|
12
|
-
const privateKey = await exportPrivateKey(keyPair.privateKey);
|
|
13
|
-
const publicKey = await exportPublicKey(keyPair.publicKey);
|
|
14
|
-
|
|
15
|
-
console.log('\n--- IMAGXP IDENTITY ---');
|
|
16
|
-
console.log(`IMAGXP_PRIVATE_KEY="${privateKey}"`);
|
|
17
|
-
console.log(`IMAGXP_PUBLIC_KEY="${publicKey}"`);
|
|
18
|
-
console.log('-----------------------\n');
|
|
19
|
-
console.log('Save these to your .env file immediately!');
|
|
20
|
-
} else {
|
|
21
|
-
console.log('Usage: imagxp generate-identity');
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
main().catch(console.error);
|
package/src/constants.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 1: Protocol Constants
|
|
3
|
-
* These values are immutable and defined by the IMAGXP Specification.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const IMAGXP_VERSION = '1.1';
|
|
7
|
-
|
|
8
|
-
// The path where Agents MUST host their public key to prove identity.
|
|
9
|
-
// Example: https://bot.openai.com/.well-known/imagxp-agent.json
|
|
10
|
-
export const WELL_KNOWN_AGENT_PATH = '/.well-known/imagxp-agent.json';
|
|
11
|
-
|
|
12
|
-
// HTTP Headers used for the handshake
|
|
13
|
-
export const HEADERS = {
|
|
14
|
-
// Transport: The signed payload (Base64 encoded JSON of ProtocolHeader)
|
|
15
|
-
PAYLOAD: 'x-imagxp-payload',
|
|
16
|
-
// Transport: The cryptographic signature (Hex)
|
|
17
|
-
SIGNATURE: 'x-imagxp-signature',
|
|
18
|
-
// Transport: The Agent's Public Key (Base64 SPKI)
|
|
19
|
-
PUBLIC_KEY: 'x-imagxp-public-key',
|
|
20
|
-
|
|
21
|
-
// Informational / Legacy (Optional if Payload is present)
|
|
22
|
-
AGENT_ID: 'x-imagxp-agent-id',
|
|
23
|
-
TIMESTAMP: 'x-imagxp-timestamp',
|
|
24
|
-
ALGORITHM: 'x-imagxp-alg',
|
|
25
|
-
|
|
26
|
-
// v1.1 Addition: Provenance (Server to Agent)
|
|
27
|
-
CONTENT_ORIGIN: 'x-imagxp-content-origin',
|
|
28
|
-
PROVENANCE_SIG: 'x-imagxp-provenance-sig',
|
|
29
|
-
|
|
30
|
-
// v1.2 Proof of Value
|
|
31
|
-
PROOF_TOKEN: 'x-imagxp-proof',
|
|
32
|
-
|
|
33
|
-
// v1.2 Payment Credential (The "Digital Receipt")
|
|
34
|
-
PAYMENT_CREDENTIAL: 'x-imagxp-credential',
|
|
35
|
-
|
|
36
|
-
// v1.2 Quality Feedback (The "Dispute Token")
|
|
37
|
-
FEEDBACK: 'x-imagxp-feedback'
|
|
38
|
-
} as const;
|
|
39
|
-
|
|
40
|
-
// Cryptographic Settings
|
|
41
|
-
export const CRYPTO_CONFIG = {
|
|
42
|
-
ALGORITHM_NAME: 'ECDSA',
|
|
43
|
-
CURVE: 'P-256',
|
|
44
|
-
HASH: 'SHA-256',
|
|
45
|
-
} as const;
|
|
46
|
-
|
|
47
|
-
// Tolerance
|
|
48
|
-
export const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000; // 5 minutes
|
package/src/crypto.ts
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 1: Cryptographic Primitives
|
|
3
|
-
* Implementation of ECDSA P-256 signing/verification.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export async function generateKeyPair(): Promise<CryptoKeyPair> {
|
|
7
|
-
// Uses standard Web Crypto API (Node 19+ compatible)
|
|
8
|
-
return await crypto.subtle.generateKey(
|
|
9
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
10
|
-
true,
|
|
11
|
-
["sign", "verify"]
|
|
12
|
-
);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function signData(privateKey: CryptoKey, data: string): Promise<string> {
|
|
16
|
-
const encoder = new TextEncoder();
|
|
17
|
-
const encoded = encoder.encode(data);
|
|
18
|
-
const signature = await crypto.subtle.sign(
|
|
19
|
-
{ name: "ECDSA", hash: { name: "SHA-256" } },
|
|
20
|
-
privateKey,
|
|
21
|
-
encoded as any
|
|
22
|
-
);
|
|
23
|
-
return bufToHex(signature);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export async function verifySignature(publicKey: CryptoKey, data: string, signatureHex: string): Promise<boolean> {
|
|
27
|
-
const encoder = new TextEncoder();
|
|
28
|
-
const encodedData = encoder.encode(data);
|
|
29
|
-
const signatureBytes = hexToBuf(signatureHex);
|
|
30
|
-
|
|
31
|
-
console.log(" 🔐 [IMAGXP Crypto] Verifying ECDSA P-256 Signature...");
|
|
32
|
-
|
|
33
|
-
const isValid = await crypto.subtle.verify(
|
|
34
|
-
{ name: "ECDSA", hash: { name: "SHA-256" } },
|
|
35
|
-
publicKey,
|
|
36
|
-
signatureBytes as any,
|
|
37
|
-
encodedData as any
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
console.log(` ${isValid ? "✅" : "❌"} [IMAGXP Crypto] Signature Result: ${isValid ? "VALID" : "INVALID"}`);
|
|
41
|
-
return isValid;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function exportPublicKey(key: CryptoKey): Promise<string> {
|
|
45
|
-
const exported = await crypto.subtle.exportKey("spki", key);
|
|
46
|
-
return btoa(String.fromCharCode(...new Uint8Array(exported)));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function importPublicKey(keyData: string): Promise<CryptoKey> {
|
|
50
|
-
const binaryString = atob(keyData);
|
|
51
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
52
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
53
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return await crypto.subtle.importKey(
|
|
57
|
-
"spki",
|
|
58
|
-
bytes,
|
|
59
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
60
|
-
true,
|
|
61
|
-
["verify"]
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function exportPrivateKey(key: CryptoKey): Promise<string> {
|
|
66
|
-
const exported = await crypto.subtle.exportKey("pkcs8", key);
|
|
67
|
-
return btoa(String.fromCharCode(...new Uint8Array(exported)));
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function importPrivateKey(keyData: string): Promise<CryptoKey> {
|
|
71
|
-
const binaryString = atob(keyData);
|
|
72
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
73
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
74
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return await crypto.subtle.importKey(
|
|
78
|
-
"pkcs8",
|
|
79
|
-
bytes,
|
|
80
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
81
|
-
true,
|
|
82
|
-
["sign"]
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Helpers
|
|
87
|
-
function bufToHex(buffer: ArrayBuffer): string {
|
|
88
|
-
return Array.from(new Uint8Array(buffer))
|
|
89
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
90
|
-
.join('');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
function hexToBuf(hex: string): Uint8Array {
|
|
94
|
-
return new Uint8Array(hex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
|
|
95
|
-
}
|
package/src/express.ts
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 3: Framework Adapters
|
|
3
|
-
* Zero-friction integration for Express/Node.js.
|
|
4
|
-
*/
|
|
5
|
-
import { IMAGXPPublisher } from './publisher.js';
|
|
6
|
-
import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types.js';
|
|
7
|
-
import { generateKeyPair, importPrivateKey, importPublicKey } from './crypto.js';
|
|
8
|
-
|
|
9
|
-
declare const process: any;
|
|
10
|
-
|
|
11
|
-
export interface IMAGXPConfig {
|
|
12
|
-
policy: Omit<AccessPolicy, 'version'>;
|
|
13
|
-
meta: {
|
|
14
|
-
origin: keyof typeof ContentOrigin;
|
|
15
|
-
paymentPointer?: string;
|
|
16
|
-
};
|
|
17
|
-
strategy?: UnauthenticatedStrategy;
|
|
18
|
-
// Optional: Provide a Redis/Memcached adapter here for production
|
|
19
|
-
cache?: IdentityCache;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class IMAGXP {
|
|
23
|
-
private publisher: IMAGXPPublisher;
|
|
24
|
-
private origin: ContentOrigin;
|
|
25
|
-
private ready: Promise<void>;
|
|
26
|
-
|
|
27
|
-
private constructor(config: IMAGXPConfig) {
|
|
28
|
-
this.publisher = new IMAGXPPublisher(
|
|
29
|
-
{ version: '1.1', ...config.policy } as AccessPolicy,
|
|
30
|
-
config.strategy || 'PASSIVE',
|
|
31
|
-
config.cache
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
this.origin = ContentOrigin[config.meta.origin];
|
|
35
|
-
|
|
36
|
-
const publisher = this.publisher;
|
|
37
|
-
this.ready = (async () => {
|
|
38
|
-
let keys: CryptoKeyPair | null = null;
|
|
39
|
-
if (process.env.IMAGXP_PRIVATE_KEY && process.env.IMAGXP_PUBLIC_KEY) {
|
|
40
|
-
try {
|
|
41
|
-
const privateKey = await importPrivateKey(process.env.IMAGXP_PRIVATE_KEY);
|
|
42
|
-
const publicKey = await importPublicKey(process.env.IMAGXP_PUBLIC_KEY);
|
|
43
|
-
keys = { privateKey, publicKey };
|
|
44
|
-
} catch (e) { console.error("IMAGXP: Failed to load keys from env", e); }
|
|
45
|
-
}
|
|
46
|
-
if (!keys) keys = await generateKeyPair();
|
|
47
|
-
await publisher.initialize(keys);
|
|
48
|
-
})();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
static init(config: IMAGXPConfig): IMAGXP {
|
|
52
|
-
return new IMAGXP(config);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Express Middleware
|
|
57
|
-
*/
|
|
58
|
-
middleware() {
|
|
59
|
-
return async (req: any, res: any, next: any) => {
|
|
60
|
-
await this.ready;
|
|
61
|
-
|
|
62
|
-
// Normalize headers to lowercase dictionary
|
|
63
|
-
const headers: Record<string, string> = {};
|
|
64
|
-
Object.keys(req.headers).forEach(key => {
|
|
65
|
-
headers[key.toLowerCase()] = req.headers[key] as string;
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
// Retrieve Raw Payload if available (optional but good for crypto)
|
|
69
|
-
// Note: Express body parsing might interfere, so we usually rely on the header content.
|
|
70
|
-
const rawPayload = headers['x-imagxp-payload'];
|
|
71
|
-
|
|
72
|
-
// Evaluate Visitor
|
|
73
|
-
const result = await this.publisher.evaluateVisitor(headers, rawPayload);
|
|
74
|
-
|
|
75
|
-
// Enforce Decision
|
|
76
|
-
if (!result.allowed) {
|
|
77
|
-
res.status(result.status).json({
|
|
78
|
-
error: result.reason,
|
|
79
|
-
visitor_type: result.visitorType,
|
|
80
|
-
proof_used: result.proofUsed
|
|
81
|
-
});
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Inject Provenance Headers (For the humans/agents that got through)
|
|
86
|
-
const respHeaders = await this.publisher.generateResponseHeaders(this.origin);
|
|
87
|
-
Object.entries(respHeaders).forEach(([k, v]) => {
|
|
88
|
-
res.setHeader(k, v);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
// Attach metadata to request for downstream use
|
|
92
|
-
req.aamp = {
|
|
93
|
-
verified: result.visitorType === 'VERIFIED_AGENT',
|
|
94
|
-
type: result.visitorType,
|
|
95
|
-
...result.metadata
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
next();
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
discoveryHandler() {
|
|
103
|
-
return (req: any, res: any) => {
|
|
104
|
-
res.setHeader('Content-Type', 'application/json');
|
|
105
|
-
res.send(JSON.stringify(this.publisher.getPolicy(), null, 2));
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* IMAGXP SDK Public API
|
|
3
|
-
*
|
|
4
|
-
* This is the main entry point for the library.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export * from './types.js';
|
|
8
|
-
export * from './constants.js';
|
|
9
|
-
export * from './agent.js';
|
|
10
|
-
export * from './publisher.js';
|
|
11
|
-
export * from './crypto.js';
|
|
12
|
-
export * from './express.js'; // Node.js / Express Adapter
|
|
13
|
-
export { IMAGXPNext } from './nextjs.js'; // Serverless / Next.js Adapter
|
package/src/nextjs.ts
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 3: Framework Adapters
|
|
3
|
-
* Serverless integration for Next.js (App Router & API Routes).
|
|
4
|
-
*/
|
|
5
|
-
import { IMAGXPPublisher } from './publisher.js';
|
|
6
|
-
import { AccessPolicy, ContentOrigin, UnauthenticatedStrategy, IdentityCache } from './types.js';
|
|
7
|
-
import { generateKeyPair, importPrivateKey, importPublicKey } from './crypto.js';
|
|
8
|
-
|
|
9
|
-
declare const process: any;
|
|
10
|
-
|
|
11
|
-
type NextRequest = any;
|
|
12
|
-
type NextResponse = any;
|
|
13
|
-
|
|
14
|
-
const createJsonResponse = (body: any, status = 200) => {
|
|
15
|
-
return new Response(JSON.stringify(body), {
|
|
16
|
-
status,
|
|
17
|
-
headers: { 'Content-Type': 'application/json' }
|
|
18
|
-
});
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export interface IMAGXPConfig {
|
|
22
|
-
policy: Omit<AccessPolicy, 'version'>;
|
|
23
|
-
meta: {
|
|
24
|
-
origin: keyof typeof ContentOrigin;
|
|
25
|
-
paymentPointer?: string;
|
|
26
|
-
};
|
|
27
|
-
strategy?: UnauthenticatedStrategy;
|
|
28
|
-
// Optional: Provide a KV/Redis adapter here for production
|
|
29
|
-
cache?: IdentityCache;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export class IMAGXPNext {
|
|
33
|
-
private publisher: IMAGXPPublisher;
|
|
34
|
-
private origin: ContentOrigin;
|
|
35
|
-
private ready: Promise<void>;
|
|
36
|
-
|
|
37
|
-
private constructor(config: IMAGXPConfig) {
|
|
38
|
-
this.publisher = new IMAGXPPublisher(
|
|
39
|
-
{ version: '1.1', ...config.policy } as AccessPolicy,
|
|
40
|
-
config.strategy || 'PASSIVE',
|
|
41
|
-
config.cache
|
|
42
|
-
);
|
|
43
|
-
this.origin = ContentOrigin[config.meta.origin];
|
|
44
|
-
|
|
45
|
-
const publisher = this.publisher;
|
|
46
|
-
this.ready = (async () => {
|
|
47
|
-
let keys: CryptoKeyPair | null = null;
|
|
48
|
-
if (process.env.IMAGXP_PRIVATE_KEY && process.env.IMAGXP_PUBLIC_KEY) {
|
|
49
|
-
try {
|
|
50
|
-
const privateKey = await importPrivateKey(process.env.IMAGXP_PRIVATE_KEY);
|
|
51
|
-
const publicKey = await importPublicKey(process.env.IMAGXP_PUBLIC_KEY);
|
|
52
|
-
keys = { privateKey, publicKey };
|
|
53
|
-
} catch (e) { console.error("IMAGXP: Failed to load keys from env", e); }
|
|
54
|
-
}
|
|
55
|
-
if (!keys) keys = await generateKeyPair();
|
|
56
|
-
await publisher.initialize(keys);
|
|
57
|
-
})();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
static init(config: IMAGXPConfig): IMAGXPNext {
|
|
61
|
-
return new IMAGXPNext(config);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Serverless Route Wrapper
|
|
67
|
-
*/
|
|
68
|
-
withProtection(handler: (req: NextRequest) => Promise<NextResponse>) {
|
|
69
|
-
return async (req: NextRequest) => {
|
|
70
|
-
await this.ready;
|
|
71
|
-
|
|
72
|
-
// Extract Headers map
|
|
73
|
-
const headers: Record<string, string> = {};
|
|
74
|
-
req.headers.forEach((value: string, key: string) => {
|
|
75
|
-
headers[key.toLowerCase()] = value;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Evaluate
|
|
79
|
-
const result = await this.publisher.evaluateVisitor(headers, headers['x-imagxp-payload']);
|
|
80
|
-
|
|
81
|
-
if (!result.allowed) {
|
|
82
|
-
return createJsonResponse({
|
|
83
|
-
error: result.reason,
|
|
84
|
-
visitor_type: result.visitorType,
|
|
85
|
-
proof_used: result.proofUsed
|
|
86
|
-
}, result.status);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Execute Handler
|
|
90
|
-
const response = await handler(req);
|
|
91
|
-
|
|
92
|
-
// Inject Provenance
|
|
93
|
-
const imagxpHeaders = await this.publisher.generateResponseHeaders(this.origin);
|
|
94
|
-
if (response && response.headers) {
|
|
95
|
-
Object.entries(imagxpHeaders).forEach(([k, v]) => {
|
|
96
|
-
response.headers.set(k, v);
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return response;
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
discoveryHandler() {
|
|
105
|
-
return async () => {
|
|
106
|
-
return createJsonResponse(this.publisher.getPolicy());
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
}
|
package/src/proof.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
2
|
-
|
|
3
|
-
// In-memory cache for JWKS to avoid repeated fetches
|
|
4
|
-
// Jose's createRemoteJWKSet handles caching/cooldowns internally.
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Verifies a JWT (Proof Token or Payment Credential) using JWKS.
|
|
8
|
-
*
|
|
9
|
-
* @param token The JWT string
|
|
10
|
-
* @param jwksUrl The URL to fetch Public Keys
|
|
11
|
-
* @param issuer The expected issuer
|
|
12
|
-
* @param audience The expected audience range
|
|
13
|
-
*/
|
|
14
|
-
export async function verifyJwt(
|
|
15
|
-
token: string,
|
|
16
|
-
jwksUrl: string,
|
|
17
|
-
issuer: string,
|
|
18
|
-
audience?: string
|
|
19
|
-
): Promise<boolean> {
|
|
20
|
-
try {
|
|
21
|
-
const JWKS = createRemoteJWKSet(new URL(jwksUrl));
|
|
22
|
-
|
|
23
|
-
const { payload } = await jwtVerify(token, JWKS, {
|
|
24
|
-
issuer: issuer,
|
|
25
|
-
audience: audience // specific audience check if provided
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
// Check specific IMAGXP claims if we standardize them
|
|
29
|
-
// if (payload.type !== 'AD_IMPRESSION') return false;
|
|
30
|
-
|
|
31
|
-
return true;
|
|
32
|
-
} catch (error) {
|
|
33
|
-
// console.error("Ad Proof Verification Failed:", error);
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
package/src/publisher.ts
DELETED
|
@@ -1,482 +0,0 @@
|
|
|
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 { AccessPolicy, AccessPurpose, AgentIdentityManifest, ContentOrigin, EvaluationResult, IdentityCache, SignedAccessRequest, UnauthenticatedStrategy } from './types.js';
|
|
8
|
-
import { verifyJwt } from './proof.js';
|
|
9
|
-
import { jwtVerify, createRemoteJWKSet } from 'jose';
|
|
10
|
-
|
|
11
|
-
interface VerificationResult {
|
|
12
|
-
allowed: boolean;
|
|
13
|
-
reason: string;
|
|
14
|
-
identityVerified: boolean;
|
|
15
|
-
proofUsed?: string; // "WHITELIST", "CREDENTIAL_JWT", "AD_JWT"
|
|
16
|
-
visitorType?: string; // For audit logs
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Default In-Memory Cache (Fallback only)
|
|
21
|
-
* NOT recommended for high-traffic Serverless production.
|
|
22
|
-
*/
|
|
23
|
-
class MemoryCache implements IdentityCache {
|
|
24
|
-
private store = new Map<string, { val: string, exp: number }>();
|
|
25
|
-
|
|
26
|
-
async get(key: string): Promise<string | null> {
|
|
27
|
-
const item = this.store.get(key);
|
|
28
|
-
if (!item) return null;
|
|
29
|
-
if (Date.now() > item.exp) {
|
|
30
|
-
this.store.delete(key);
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
return item.val;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async set(key: string, value: string, ttlSeconds: number): Promise<void> {
|
|
37
|
-
this.store.set(key, {
|
|
38
|
-
val: value,
|
|
39
|
-
exp: Date.now() + (ttlSeconds * 1000)
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export class IMAGXPPublisher {
|
|
45
|
-
private policy: AccessPolicy;
|
|
46
|
-
private keyPair: CryptoKeyPair | null = null;
|
|
47
|
-
private unauthenticatedStrategy: UnauthenticatedStrategy;
|
|
48
|
-
private cache: IdentityCache;
|
|
49
|
-
|
|
50
|
-
// Default TTL: 1 Hour
|
|
51
|
-
private readonly CACHE_TTL_SECONDS = 3600;
|
|
52
|
-
|
|
53
|
-
constructor(
|
|
54
|
-
policy: AccessPolicy,
|
|
55
|
-
strategy: UnauthenticatedStrategy = 'PASSIVE',
|
|
56
|
-
cacheImpl?: IdentityCache
|
|
57
|
-
) {
|
|
58
|
-
this.policy = policy;
|
|
59
|
-
this.unauthenticatedStrategy = strategy;
|
|
60
|
-
this.cache = cacheImpl || new MemoryCache();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async initialize(keyPair: CryptoKeyPair) {
|
|
64
|
-
this.keyPair = keyPair;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
getPolicy(): AccessPolicy {
|
|
68
|
-
return this.policy;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Main Entry Point: Evaluate ANY visitor (Human, Bot, or Agent)
|
|
73
|
-
* STAGE 1: IDENTITY (Strict)
|
|
74
|
-
* STAGE 2: POLICY (Permissions)
|
|
75
|
-
* STAGE 3: ACCESS (HQ Content)
|
|
76
|
-
*/
|
|
77
|
-
async evaluateVisitor(
|
|
78
|
-
reqHeaders: Record<string, string | undefined>,
|
|
79
|
-
rawPayload?: string
|
|
80
|
-
): Promise<EvaluationResult> {
|
|
81
|
-
console.log(`\n--- [IMAGXP LOG START] New Request ---`);
|
|
82
|
-
|
|
83
|
-
// --- STAGE 1: IDENTITY VERIFICATION ---
|
|
84
|
-
console.log(`[IDENTITY] 🔍 Checking Identity Headers...`);
|
|
85
|
-
|
|
86
|
-
const hasImagxp = reqHeaders[HEADERS.PAYLOAD] && reqHeaders[HEADERS.SIGNATURE] && reqHeaders[HEADERS.PUBLIC_KEY];
|
|
87
|
-
|
|
88
|
-
if (hasImagxp) {
|
|
89
|
-
// It claims to be an Agent. Verify it STRICTLY.
|
|
90
|
-
return await this.handleAgentStrict(reqHeaders, rawPayload);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// If NO IMAGXP Headers -> FAIL IDENTITY immediately.
|
|
94
|
-
console.log(`[IDENTITY] ❌ FAILED. No IMAGXP Headers found.`);
|
|
95
|
-
|
|
96
|
-
// For now, retaining the legacy "Passive/Hybrid" switch just to avoid breaking browser demos completely
|
|
97
|
-
// BUT logging it as a specific "Identity Fail" flow.
|
|
98
|
-
if (this.unauthenticatedStrategy === 'STRICT') {
|
|
99
|
-
console.log(`[IDENTITY] ⛔ BLOCKING. Strategy is STRICT.`);
|
|
100
|
-
return {
|
|
101
|
-
allowed: false,
|
|
102
|
-
status: 401,
|
|
103
|
-
reason: "IDENTITY_REQUIRED: Missing IMAGXP Headers.",
|
|
104
|
-
visitorType: 'UNIDENTIFIED_BOT'
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
console.log(`[IDENTITY] ⚠️ SKIPPED (Legacy Mode). Checking Browser Heuristics...`);
|
|
109
|
-
const isHuman = this.performBrowserHeuristics(reqHeaders);
|
|
110
|
-
if (isHuman) {
|
|
111
|
-
console.log(`[POLICY] 👤 ALLOWED. Browser Heuristics Passed.`);
|
|
112
|
-
return { allowed: true, status: 200, reason: "BROWSER_VERIFIED", visitorType: 'LIKELY_HUMAN' };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
console.log(`[IDENTITY] ❌ FAILED. Not a Browser, No Headers.`);
|
|
116
|
-
console.log(`[ACCESS] ⛔ BLOCKED.`);
|
|
117
|
-
return {
|
|
118
|
-
allowed: false,
|
|
119
|
-
status: 403,
|
|
120
|
-
reason: "IDENTITY_FAIL: No Identity, No Browser.",
|
|
121
|
-
visitorType: 'UNIDENTIFIED_BOT'
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Browser Heuristics (Hardened)
|
|
127
|
-
* 1. Checks Known Bot Signatures (Fast Fail)
|
|
128
|
-
* 2. Checks Trusted Upstream Signals (Cloudflare/Vercel)
|
|
129
|
-
* 3. Checks Browser Header Consistency
|
|
130
|
-
*/
|
|
131
|
-
private performBrowserHeuristics(headers: Record<string, string | undefined>): boolean {
|
|
132
|
-
const userAgent = headers['user-agent'] || '';
|
|
133
|
-
|
|
134
|
-
// A. The "Obvious Bot" Blocklist (Fast Fail)
|
|
135
|
-
const botSignatures = ['python-requests', 'curl', 'wget', 'scrapy', 'bot', 'crawler', 'spider'];
|
|
136
|
-
if (botSignatures.some(sig => userAgent.toLowerCase().includes(sig))) {
|
|
137
|
-
return false;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// B. Trusted Infrastructure Signals (The Real World Solution)
|
|
141
|
-
if (headers['cf-visitor'] || headers['cf-ray']) return true;
|
|
142
|
-
if (headers['x-vercel-id']) return true;
|
|
143
|
-
if (headers['cloudfront-viewer-address']) return true;
|
|
144
|
-
|
|
145
|
-
// C. The "Browser Fingerprint" (Fallback for direct connections)
|
|
146
|
-
const hasAcceptLanguage = !!headers['accept-language'];
|
|
147
|
-
const hasSecFetchDest = !!headers['sec-fetch-dest'];
|
|
148
|
-
const hasUpgradeInsecure = !!headers['upgrade-insecure-requests'];
|
|
149
|
-
|
|
150
|
-
if (hasAcceptLanguage && (hasSecFetchDest || hasUpgradeInsecure)) {
|
|
151
|
-
return true;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Handle IMAGXP Protocol Logic (Strict Mode)
|
|
159
|
-
*/
|
|
160
|
-
private async handleAgentStrict(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult> {
|
|
161
|
-
let agentId = "UNKNOWN";
|
|
162
|
-
|
|
163
|
-
try {
|
|
164
|
-
// 1. Decode Headers
|
|
165
|
-
const payloadHeader = reqHeaders[HEADERS.PAYLOAD]!;
|
|
166
|
-
const sigHeader = reqHeaders[HEADERS.SIGNATURE]!;
|
|
167
|
-
const keyHeader = reqHeaders[HEADERS.PUBLIC_KEY]!;
|
|
168
|
-
|
|
169
|
-
const headerJson = atob(payloadHeader);
|
|
170
|
-
const requestHeader = JSON.parse(headerJson);
|
|
171
|
-
agentId = requestHeader.agent_id;
|
|
172
|
-
|
|
173
|
-
console.log(`[IDENTITY] 🆔 Claimed ID: ${agentId}`);
|
|
174
|
-
|
|
175
|
-
// 2. Crypto & DNS Verification
|
|
176
|
-
const signedRequest: SignedAccessRequest = {
|
|
177
|
-
header: requestHeader,
|
|
178
|
-
signature: sigHeader,
|
|
179
|
-
publicKey: keyHeader
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const agentKey = await crypto.subtle.importKey(
|
|
183
|
-
"spki",
|
|
184
|
-
new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))),
|
|
185
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
186
|
-
true,
|
|
187
|
-
["verify"]
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
// Verify Core Logic (DNS + Crypto)
|
|
191
|
-
const verification = await this.verifyRequestLogic(signedRequest, agentKey);
|
|
192
|
-
|
|
193
|
-
if (!verification.identityVerified) {
|
|
194
|
-
console.log(`[IDENTITY] ❌ FAILED. Reason: ${verification.reason}`);
|
|
195
|
-
console.log(`[ACCESS] ⛔ BLOCKED.`);
|
|
196
|
-
return { allowed: false, status: 403, reason: verification.reason, visitorType: 'UNIDENTIFIED_BOT' };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
console.log(`[IDENTITY] ✅ PASSED. DNS Binding Verified.`);
|
|
200
|
-
|
|
201
|
-
// --- STAGE 2: POLICY ENFORCEMENT ---
|
|
202
|
-
console.log(`[POLICY] 📜 Checking Permissions for ${agentId}...`);
|
|
203
|
-
|
|
204
|
-
const proofToken = reqHeaders[HEADERS.PROOF_TOKEN];
|
|
205
|
-
const paymentCredential = reqHeaders[HEADERS.PAYMENT_CREDENTIAL];
|
|
206
|
-
|
|
207
|
-
const policyResult = await this.checkPolicyStrict(requestHeader, proofToken, paymentCredential);
|
|
208
|
-
|
|
209
|
-
if (!policyResult.allowed) {
|
|
210
|
-
console.log(`[POLICY] ⛔ DENIED. Reason: ${policyResult.reason}`);
|
|
211
|
-
console.log(`[ACCESS] ⛔ BLOCKED.`);
|
|
212
|
-
return policyResult;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// --- STAGE 3: ACCESS GRANT ---
|
|
216
|
-
console.log(`[POLICY] ✅ PASSED. Requirements Met.`);
|
|
217
|
-
console.log(`[ACCESS] 🔓 GRANTED. Unlocking HQ Content.`);
|
|
218
|
-
|
|
219
|
-
return {
|
|
220
|
-
allowed: true,
|
|
221
|
-
status: 200,
|
|
222
|
-
reason: "IMAGXP_VERIFIED",
|
|
223
|
-
visitorType: 'VERIFIED_AGENT',
|
|
224
|
-
metadata: requestHeader,
|
|
225
|
-
proofUsed: policyResult.proofUsed
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
} catch (e) {
|
|
229
|
-
console.error(`[IMAGXP ERROR]`, e);
|
|
230
|
-
return { allowed: false, status: 400, reason: "INVALID_SIGNATURE", visitorType: 'UNIDENTIFIED_BOT' };
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Legacy handler kept for interface compatibility (deprecated)
|
|
235
|
-
private async handleAgent(reqHeaders: Record<string, string | undefined>, rawPayload?: string): Promise<EvaluationResult> {
|
|
236
|
-
return this.handleAgentStrict(reqHeaders, rawPayload);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* STAGE 2: POLICY ENFORCEMENT CHECK
|
|
241
|
-
*/
|
|
242
|
-
private async checkPolicyStrict(
|
|
243
|
-
requestHeader: any,
|
|
244
|
-
proofToken?: string,
|
|
245
|
-
paymentCredential?: string
|
|
246
|
-
): Promise<EvaluationResult> {
|
|
247
|
-
|
|
248
|
-
// 1. Policy Check: Purpose Ban (e.g. No Training)
|
|
249
|
-
if (requestHeader.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
|
|
250
|
-
return { allowed: false, status: 403, reason: 'POLICY_DENIED: Training not allowed.', visitorType: 'VERIFIED_AGENT' };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// 2. BROKER CHECK (New v1.1)
|
|
254
|
-
if (this.policy.monetization?.brokerUrl) {
|
|
255
|
-
const brokerUrl = this.policy.monetization.brokerUrl;
|
|
256
|
-
|
|
257
|
-
if (!paymentCredential) {
|
|
258
|
-
return { allowed: false, status: 402, reason: "PAYMENT_REQUIRED: Missing Broker Credential", visitorType: 'UNIDENTIFIED_BOT' };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const isValid = await this.verifyBrokerCred(paymentCredential, brokerUrl);
|
|
262
|
-
|
|
263
|
-
if (!isValid) {
|
|
264
|
-
return { allowed: false, status: 403, reason: "PAYMENT_DENIED: Invalid Broker Token", visitorType: 'UNIDENTIFIED_BOT' };
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// If valid, we record the "Proof Used" so we can settle later
|
|
268
|
-
return { allowed: true, status: 200, reason: "IMAGXP_PAID", visitorType: "VERIFIED_AGENT", proofUsed: `BROKER_JWT:${paymentCredential.slice(0, 10)}...` };
|
|
269
|
-
}
|
|
270
|
-
if (requestHeader.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
|
|
271
|
-
return { allowed: false, status: 403, reason: 'POLICY_DENIED: RAG not allowed.', visitorType: 'VERIFIED_AGENT' };
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// 2. Policy Check: Economics (v1.2) - Payment & Ads
|
|
275
|
-
if (this.policy.requiresPayment) {
|
|
276
|
-
let paymentSatisfied = false;
|
|
277
|
-
|
|
278
|
-
// Method A: Flexible Payment Callback (DB / Custom Logic)
|
|
279
|
-
if (this.policy.monetization?.checkPayment) {
|
|
280
|
-
const isPaid = await this.policy.monetization.checkPayment(requestHeader.agent_id, requestHeader.purpose);
|
|
281
|
-
if (isPaid) {
|
|
282
|
-
console.log(`[POLICY] 💰 Payment Verified via Callback.`);
|
|
283
|
-
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'WHITELIST_CALLBACK' };
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Method B: Payment Credentials (Unified JWT)
|
|
288
|
-
if (!paymentSatisfied && this.policy.monetization?.paymentConfig && paymentCredential) {
|
|
289
|
-
const { jwksUrl, issuer } = this.policy.monetization.paymentConfig;
|
|
290
|
-
console.log(`[POLICY] 🔐 Verifying Payment Credential (Issuer: ${issuer})...`);
|
|
291
|
-
|
|
292
|
-
const isValidCredential = await verifyJwt(paymentCredential, jwksUrl, issuer);
|
|
293
|
-
if (isValidCredential) {
|
|
294
|
-
console.log(`[POLICY] ✅ Credential Signature VALID.`);
|
|
295
|
-
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'PAYMENT_CREDENTIAL_JWT' };
|
|
296
|
-
} else {
|
|
297
|
-
console.log(`[POLICY] ❌ Credential Signature INVALID.`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Method C: Ad-Supported (Proof Verification)
|
|
302
|
-
if (!paymentSatisfied && this.policy.allowAdSupportedAccess && requestHeader.context?.ads_displayed) {
|
|
303
|
-
if (proofToken && this.policy.monetization?.adNetwork) {
|
|
304
|
-
const { jwksUrl, issuer } = this.policy.monetization.adNetwork;
|
|
305
|
-
console.log(`[POLICY] 📺 Verifying Ad Proof (Issuer: ${issuer})...`);
|
|
306
|
-
|
|
307
|
-
const isValidProof = await verifyJwt(proofToken, jwksUrl, issuer);
|
|
308
|
-
if (isValidProof) {
|
|
309
|
-
console.log(`[POLICY] ✅ Ad Proof Signature VALID.`);
|
|
310
|
-
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT', proofUsed: 'AD_PROOF_JWT' };
|
|
311
|
-
} else {
|
|
312
|
-
console.log(`[POLICY] ❌ Ad Proof Signature INVALID.`);
|
|
313
|
-
}
|
|
314
|
-
} else {
|
|
315
|
-
console.log(`[POLICY] ⚠️ Ad Proof MISSING.`);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return {
|
|
320
|
-
allowed: false,
|
|
321
|
-
status: 402,
|
|
322
|
-
reason: 'PAYMENT_REQUIRED: Whitelist, Credential, and Ad Proof checks ALL failed.',
|
|
323
|
-
visitorType: 'VERIFIED_AGENT',
|
|
324
|
-
proofUsed: 'NONE'
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// If no payment required, allow.
|
|
329
|
-
return { allowed: true, status: 200, reason: 'OK', visitorType: 'VERIFIED_AGENT' };
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
private async verifyRequestLogic(
|
|
333
|
-
request: SignedAccessRequest,
|
|
334
|
-
requestPublicKey: CryptoKey,
|
|
335
|
-
): Promise<VerificationResult> {
|
|
336
|
-
|
|
337
|
-
// 1. Replay Attack Prevention
|
|
338
|
-
const requestTime = new Date(request.header.ts).getTime();
|
|
339
|
-
if (Math.abs(Date.now() - requestTime) > MAX_CLOCK_SKEW_MS) {
|
|
340
|
-
return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew too large.', identityVerified: false };
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// 2. Crypto Verification
|
|
344
|
-
const signableString = JSON.stringify(request.header);
|
|
345
|
-
const isCryptoValid = await verifySignature(requestPublicKey, signableString, request.signature);
|
|
346
|
-
if (!isCryptoValid) return { allowed: false, reason: 'CRYPTO_FAIL: Signature invalid.', identityVerified: false };
|
|
347
|
-
|
|
348
|
-
// 3. Identity Verification (DNS Binding) with Cache
|
|
349
|
-
let identityVerified = false;
|
|
350
|
-
const claimedDomain = request.header.agent_id;
|
|
351
|
-
const pubKeyString = await exportPublicKey(requestPublicKey);
|
|
352
|
-
|
|
353
|
-
console.log(`[IDENTITY] 🔍 Verifying DNS Binding for: ${claimedDomain}`);
|
|
354
|
-
|
|
355
|
-
// Check Cache First
|
|
356
|
-
const cachedKey = await this.cache.get(claimedDomain);
|
|
357
|
-
|
|
358
|
-
if (cachedKey === pubKeyString) {
|
|
359
|
-
console.log("[IDENTITY] ⚡ Cache Hit. Identity Verified.");
|
|
360
|
-
identityVerified = true;
|
|
361
|
-
} else if (this.isDomain(claimedDomain)) {
|
|
362
|
-
// Cache Miss: Perform DNS Fetch
|
|
363
|
-
identityVerified = await this.verifyDnsBinding(claimedDomain, pubKeyString);
|
|
364
|
-
if (identityVerified) {
|
|
365
|
-
await this.cache.set(claimedDomain, pubKeyString, this.CACHE_TTL_SECONDS);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (this.policy.requireIdentityBinding && !identityVerified) {
|
|
370
|
-
return { allowed: false, reason: 'IDENTITY_FAIL: DNS Binding could not be verified.', identityVerified: false };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Return verified status so handleAgentStrict can proceed to Policy Check
|
|
374
|
-
return { allowed: true, reason: 'OK', identityVerified: identityVerified };
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
private async verifyDnsBinding(domain: string, requestKeySpki: string): Promise<boolean> {
|
|
378
|
-
try {
|
|
379
|
-
// Allow HTTP for localhost testing
|
|
380
|
-
const protocol = (domain.includes('localhost') || domain.match(/:\d+$/)) ? 'http' : 'https';
|
|
381
|
-
const url = `${protocol}://${domain}${WELL_KNOWN_AGENT_PATH}`;
|
|
382
|
-
|
|
383
|
-
console.log(` 🌍 [IMAGXP DNS] Fetching Manifest: ${url} ...`);
|
|
384
|
-
|
|
385
|
-
// In production, we need a short timeout to prevent hanging
|
|
386
|
-
const controller = new AbortController();
|
|
387
|
-
const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5s max for DNS check
|
|
388
|
-
|
|
389
|
-
const response = await fetch(url, { signal: controller.signal });
|
|
390
|
-
clearTimeout(timeoutId);
|
|
391
|
-
|
|
392
|
-
if (!response.ok) {
|
|
393
|
-
console.log(` ❌ [IMAGXP DNS] Fetch Failed: ${response.status}`);
|
|
394
|
-
return false;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const manifest = await response.json() as AgentIdentityManifest;
|
|
398
|
-
console.log(` 📄 [IMAGXP DNS] Manifest received. Agent ID: ${manifest.agent_id}`);
|
|
399
|
-
|
|
400
|
-
// CHECK 1: Does the manifest actually belong to the domain?
|
|
401
|
-
if (manifest.agent_id !== domain) {
|
|
402
|
-
console.log(` ❌ [IMAGXP DNS] Mismatch: Manifest ID ${manifest.agent_id} != Claimed ${domain}`);
|
|
403
|
-
return false;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// CHECK 2: Does the key match?
|
|
407
|
-
if (manifest.public_key !== requestKeySpki) {
|
|
408
|
-
console.log(` ❌ [IMAGXP DNS] Key Mismatch: DNS Key != Request Key`);
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
console.log(` ✅ [IMAGXP DNS] Identity Confirmed.`);
|
|
413
|
-
return true;
|
|
414
|
-
} catch (e: any) {
|
|
415
|
-
console.log(` ❌ [IMAGXP DNS] Error: ${e.message}`);
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/**
|
|
421
|
-
* NEW: Verify a Broker-Issued Token (JWT)
|
|
422
|
-
* Checks if the request contains a valid "Visa" from the Broker.
|
|
423
|
-
*/
|
|
424
|
-
private async verifyBrokerCred(credential: string, brokerUrl: string): Promise<boolean> {
|
|
425
|
-
try {
|
|
426
|
-
// 1. Fetch Broker's Public Keys (JWKS)
|
|
427
|
-
const JWKS = createRemoteJWKSet(new URL(`${brokerUrl}/.well-known/jwks.json`));
|
|
428
|
-
|
|
429
|
-
// 2. Verify the Token Signature
|
|
430
|
-
const { payload } = await jwtVerify(credential, JWKS, {
|
|
431
|
-
issuer: brokerUrl, // Ensure it came from THE Broker
|
|
432
|
-
clockTolerance: 5 // Allow 5s clock skew
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
console.log(`[BROKER] 💰 Valid Payment Token from ${payload.iss} for amount ${payload.amount}`);
|
|
436
|
-
return true;
|
|
437
|
-
|
|
438
|
-
} catch (e: any) {
|
|
439
|
-
console.warn(`[BROKER] ❌ Invalid Token:`, e.message);
|
|
440
|
-
return false;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
private isDomain(s: string): boolean {
|
|
445
|
-
// Basic regex, allows localhost with ports
|
|
446
|
-
return /^[a-zA-Z0-9.-]+(:\d+)?$/.test(s) || /^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(s);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
async generateResponseHeaders(origin: ContentOrigin): Promise<Record<string, string>> {
|
|
450
|
-
if (!this.keyPair) throw new Error("Publisher keys not initialized");
|
|
451
|
-
const payload = JSON.stringify({ origin, ts: Date.now() });
|
|
452
|
-
const signature = await signData(this.keyPair.privateKey, payload);
|
|
453
|
-
return {
|
|
454
|
-
[HEADERS.CONTENT_ORIGIN]: origin,
|
|
455
|
-
[HEADERS.PROVENANCE_SIG]: signature
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
/**
|
|
460
|
-
* Handling Quality Feedback (The "Dispute" Layer)
|
|
461
|
-
* This runs when an Agent sends 'x-imagxp-feedback'.
|
|
462
|
-
*/
|
|
463
|
-
private async handleFeedback(token: string, headers: Record<string, string | undefined>) {
|
|
464
|
-
// NOTE: In production, you would fetch the Agent's specific key.
|
|
465
|
-
// For now, we assume standard Discovery or a centralized Key Set (like adNetwork).
|
|
466
|
-
// Ideally, the SDK config should have a 'qualityOracle' key set.
|
|
467
|
-
|
|
468
|
-
// 1. We just Decode it to Log it (Verification is optional but recommended)
|
|
469
|
-
try {
|
|
470
|
-
const parts = token.split('.');
|
|
471
|
-
const payload = JSON.parse(atob(parts[1]));
|
|
472
|
-
|
|
473
|
-
console.log(`\n📢 [IMAGXP QUALITY ALERT] Feedback Received from ${payload.agent_id}`);
|
|
474
|
-
console.log(` Reason: ${payload.reason} | Score: ${payload.quality_score}`);
|
|
475
|
-
console.log(` Resource: ${payload.url}`);
|
|
476
|
-
console.log(` (Signature available for dispute evidence)`);
|
|
477
|
-
|
|
478
|
-
} catch (e) {
|
|
479
|
-
console.log(` ⚠️ [IMAGXP Warning] Malformed Feedback Token.`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 1: Protocol Definitions
|
|
3
|
-
* Shared types used by both Agent and Publisher.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export enum AccessPurpose {
|
|
7
|
-
CRAWL_TRAINING = 'CRAWL_TRAINING',
|
|
8
|
-
RAG_RETRIEVAL = 'RAG_RETRIEVAL',
|
|
9
|
-
SUMMARY = 'SUMMARY',
|
|
10
|
-
QUOTATION = 'QUOTATION',
|
|
11
|
-
EMBEDDING = 'EMBEDDING'
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export enum ContentOrigin {
|
|
15
|
-
HUMAN = 'HUMAN', // Created by humans. High training value.
|
|
16
|
-
SYNTHETIC = 'SYNTHETIC', // Created by AI. Risk of model collapse.
|
|
17
|
-
HYBRID = 'HYBRID' // Edited by humans, drafted by AI.
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export enum QualityFlag {
|
|
21
|
-
SEO_SPAM = 'SEO_SPAM',
|
|
22
|
-
INACCURATE = 'INACCURATE',
|
|
23
|
-
HATE_SPEECH = 'HATE_SPEECH',
|
|
24
|
-
HIGH_QUALITY = 'HIGH_QUALITY'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* DNS Identity Manifest
|
|
29
|
-
* Hosted at: https://{agent_id}/.well-known/imagxp-agent.json
|
|
30
|
-
*/
|
|
31
|
-
export interface AgentIdentityManifest {
|
|
32
|
-
agent_id: string; // e.g. "bot.openai.com"
|
|
33
|
-
public_key: string; // Base64 SPKI
|
|
34
|
-
contact_email?: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* PRODUCTION INFRASTRUCTURE: Cache Interface
|
|
39
|
-
* Required for Serverless/Edge environments to prevent repeated DNS fetches.
|
|
40
|
-
*/
|
|
41
|
-
export interface IdentityCache {
|
|
42
|
-
get(key: string): Promise<string | null>; // Returns stored PublicKey
|
|
43
|
-
set(key: string, value: string, ttlSeconds: number): Promise<void>;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Optional Monetization (The Settlement Layer)
|
|
48
|
-
*/
|
|
49
|
-
/**
|
|
50
|
-
* Optional Monetization (The Settlement Layer)
|
|
51
|
-
*/
|
|
52
|
-
export interface MonetizationConfig {
|
|
53
|
-
// Method 1: Payments (Flexible Callback)
|
|
54
|
-
// Developers implement their own logic (Database check, CMS lookup, etc.)
|
|
55
|
-
// Returns TRUE if the agent is a paid subscriber for this specific purpose.
|
|
56
|
-
checkPayment?: (agentId: string, purpose: string) => boolean | Promise<boolean>;
|
|
57
|
-
|
|
58
|
-
// Method 2: Ads (Proof Verification)
|
|
59
|
-
// Configuration to verify tokens from your Ad Provider (e.g. Google)
|
|
60
|
-
adNetwork?: {
|
|
61
|
-
jwksUrl: string; // e.g. "https://www.googleapis.com/oauth2/v3/certs"
|
|
62
|
-
issuer: string; // e.g. "https://accounts.google.com"
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// Method 3: Broker Integration (NEW)
|
|
66
|
-
// For third-party clearing houses (AdSense for Data)
|
|
67
|
-
brokerUrl?: string; // e.g. "https://broker.imagxp.network"
|
|
68
|
-
|
|
69
|
-
// Method 4: Payment Credentials (Unified JWT)
|
|
70
|
-
// Verifies "x-imagxp-credential" for Broker or Direct payments.
|
|
71
|
-
paymentConfig?: {
|
|
72
|
-
jwksUrl: string; // e.g. "https://my-site.com/.well-known/jwks.json"
|
|
73
|
-
issuer: string; // e.g. "my-site.com"
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Handling Non-IMAGXP Visitors
|
|
81
|
-
*
|
|
82
|
-
* PASSIVE: Allow everyone (Legacy web behavior).
|
|
83
|
-
* HYBRID: Allow verified Agents AND likely Humans (Browser Heuristics). Block bots.
|
|
84
|
-
* STRICT: Allow ONLY verified IMAGXP Agents. (API Mode).
|
|
85
|
-
*/
|
|
86
|
-
export type UnauthenticatedStrategy = 'PASSIVE' | 'HYBRID' | 'STRICT';
|
|
87
|
-
|
|
88
|
-
export interface AccessPolicy {
|
|
89
|
-
version: '1.1';
|
|
90
|
-
allowTraining: boolean;
|
|
91
|
-
allowRAG: boolean;
|
|
92
|
-
attributionRequired: boolean;
|
|
93
|
-
|
|
94
|
-
// Economic Signals
|
|
95
|
-
allowAdSupportedAccess: boolean;
|
|
96
|
-
requiresPayment: boolean;
|
|
97
|
-
paymentPointer?: string;
|
|
98
|
-
|
|
99
|
-
// Identity Strictness
|
|
100
|
-
requireIdentityBinding?: boolean;
|
|
101
|
-
|
|
102
|
-
// V1.1: Optional Settlement Info
|
|
103
|
-
monetization?: MonetizationConfig;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export interface ProtocolHeader {
|
|
107
|
-
v: '1.1';
|
|
108
|
-
ts: string;
|
|
109
|
-
agent_id: string;
|
|
110
|
-
resource: string;
|
|
111
|
-
purpose: AccessPurpose;
|
|
112
|
-
context: {
|
|
113
|
-
ads_displayed: boolean;
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface SignedAccessRequest {
|
|
118
|
-
header: ProtocolHeader;
|
|
119
|
-
signature: string;
|
|
120
|
-
publicKey?: string;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export interface FeedbackSignal {
|
|
124
|
-
target_resource: string;
|
|
125
|
-
agent_id: string;
|
|
126
|
-
quality_score: number;
|
|
127
|
-
flags: QualityFlag[];
|
|
128
|
-
timestamp: string;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Result of the full evaluation pipeline
|
|
132
|
-
// Result of the full evaluation pipeline
|
|
133
|
-
export interface EvaluationResult {
|
|
134
|
-
allowed: boolean;
|
|
135
|
-
status: 200 | 400 | 401 | 402 | 403;
|
|
136
|
-
reason: string;
|
|
137
|
-
visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
|
|
138
|
-
metadata?: any;
|
|
139
|
-
payment_status?: 'PAID_SUBSCRIBER' | 'AD_FUNDED' | 'UNPAID';
|
|
140
|
-
proofUsed?: string;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Signed Quality Feedback (The "Report Card")
|
|
144
|
-
export interface FeedbackSignalToken {
|
|
145
|
-
url: string; // The resource being flagged
|
|
146
|
-
agent_id: string; // Who is flagging it (e.g. "bot.openai.com")
|
|
147
|
-
quality_score: number; // 0.0 to 1.0
|
|
148
|
-
reason: string; // e.g. "SEO_SPAM", "HATE_SPEECH"
|
|
149
|
-
timestamp: number;
|
|
150
|
-
}
|
package/test/handshake.spec.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration Test: Protocol Handshake
|
|
3
|
-
* Run using: npm test
|
|
4
|
-
* Location: sdk/typescript/test/handshake.spec.ts
|
|
5
|
-
*/
|
|
6
|
-
import { IMAGXPAgent } from '../src/agent.js';
|
|
7
|
-
import { IMAGXPPublisher } from '../src/publisher.js';
|
|
8
|
-
import { AccessPurpose } from '../src/types.js';
|
|
9
|
-
import { HEADERS } from '../src/constants.js';
|
|
10
|
-
|
|
11
|
-
async function runTest() {
|
|
12
|
-
console.log("--- STARTING IMAGXP HANDSHAKE TEST ---");
|
|
13
|
-
|
|
14
|
-
// 1. Setup Publisher with Economic Policy
|
|
15
|
-
const publisher = new IMAGXPPublisher({
|
|
16
|
-
version: '1.1',
|
|
17
|
-
allowTraining: false,
|
|
18
|
-
allowRAG: true,
|
|
19
|
-
attributionRequired: true,
|
|
20
|
-
requiresPayment: true, // Publisher wants money...
|
|
21
|
-
allowAdSupportedAccess: true, // ...OR ads
|
|
22
|
-
paymentPointer: '$wallet.example.com/publisher'
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// 2. Initialize Agent
|
|
26
|
-
const agent = new IMAGXPAgent();
|
|
27
|
-
await agent.initialize();
|
|
28
|
-
|
|
29
|
-
// TEST CASE A: Requesting RAG without Ads (Should FAIL due to Payment Requirement)
|
|
30
|
-
console.log("\n[TEST A] Requesting RAG (No Ads)...");
|
|
31
|
-
const reqA = await agent.createAccessRequest('/doc/1', AccessPurpose.RAG_RETRIEVAL, { adsDisplayed: false });
|
|
32
|
-
|
|
33
|
-
const payloadA = JSON.stringify(reqA.header);
|
|
34
|
-
const headersA = {
|
|
35
|
-
[HEADERS.PAYLOAD]: btoa(payloadA),
|
|
36
|
-
[HEADERS.SIGNATURE]: reqA.signature,
|
|
37
|
-
[HEADERS.PUBLIC_KEY]: reqA.publicKey!
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const resA = await publisher.evaluateVisitor(headersA, payloadA);
|
|
41
|
-
console.log("Result A (Expect Deny):", resA);
|
|
42
|
-
if (resA.allowed) throw new Error("Test A Failed (Should require payment)");
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// TEST CASE B: Requesting RAG WITH Ads (Should SUCCEED via Exemption)
|
|
46
|
-
console.log("\n[TEST B] Requesting RAG (With Ads)...");
|
|
47
|
-
const reqB = await agent.createAccessRequest('/doc/1', AccessPurpose.RAG_RETRIEVAL, { adsDisplayed: true });
|
|
48
|
-
|
|
49
|
-
const payloadB = JSON.stringify(reqB.header);
|
|
50
|
-
const headersB = {
|
|
51
|
-
[HEADERS.PAYLOAD]: btoa(payloadB),
|
|
52
|
-
[HEADERS.SIGNATURE]: reqB.signature,
|
|
53
|
-
[HEADERS.PUBLIC_KEY]: reqB.publicKey!
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
const resB = await publisher.evaluateVisitor(headersB, payloadB);
|
|
57
|
-
console.log("Result B (Expect Allow):", resB);
|
|
58
|
-
if (!resB.allowed) throw new Error("Test B Failed (Should allow via Ad exemption)");
|
|
59
|
-
|
|
60
|
-
console.log("\n--- ALL TESTS PASSED ---");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
runTest().catch(console.error);
|
package/tsconfig.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": [
|
|
7
|
-
"ES2020",
|
|
8
|
-
"DOM"
|
|
9
|
-
],
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"outDir": "./dist",
|
|
12
|
-
"rootDir": "./src",
|
|
13
|
-
"strict": true,
|
|
14
|
-
"esModuleInterop": true,
|
|
15
|
-
"skipLibCheck": true,
|
|
16
|
-
"forceConsistentCasingInFileNames": true
|
|
17
|
-
},
|
|
18
|
-
"include": [
|
|
19
|
-
"src/**/*"
|
|
20
|
-
]
|
|
21
|
-
}
|