@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.
- package/README.md +103 -0
- package/dist/agent.d.ts +30 -0
- package/dist/agent.js +65 -0
- package/dist/constants.d.ts +25 -0
- package/dist/constants.js +38 -0
- package/dist/crypto.d.ts +9 -0
- package/dist/crypto.js +44 -0
- package/dist/express.d.ts +22 -0
- package/dist/express.js +64 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +12 -0
- package/dist/nextjs.d.ts +25 -0
- package/dist/nextjs.js +60 -0
- package/dist/proof.d.ts +9 -0
- package/dist/proof.js +27 -0
- package/dist/publisher.d.ts +48 -0
- package/dist/publisher.js +385 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.js +25 -0
- package/package.json +53 -0
- package/src/agent.ts +88 -0
- package/src/constants.ts +48 -0
- package/src/crypto.ts +74 -0
- package/src/express.ts +96 -0
- package/src/index.ts +13 -0
- package/src/nextjs.ts +94 -0
- package/src/proof.ts +36 -0
- package/src/publisher.ts +482 -0
- package/src/types.ts +150 -0
- package/test/handshake.spec.ts +63 -0
- package/tsconfig.json +21 -0
package/src/constants.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
// Helpers
|
|
66
|
+
function bufToHex(buffer: ArrayBuffer): string {
|
|
67
|
+
return Array.from(new Uint8Array(buffer))
|
|
68
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
69
|
+
.join('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function hexToBuf(hex: string): Uint8Array {
|
|
73
|
+
return new Uint8Array(hex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
|
|
74
|
+
}
|
package/src/express.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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 } from './crypto.js';
|
|
8
|
+
|
|
9
|
+
export interface IMAGXPConfig {
|
|
10
|
+
policy: Omit<AccessPolicy, 'version'>;
|
|
11
|
+
meta: {
|
|
12
|
+
origin: keyof typeof ContentOrigin;
|
|
13
|
+
paymentPointer?: string;
|
|
14
|
+
};
|
|
15
|
+
strategy?: UnauthenticatedStrategy;
|
|
16
|
+
// Optional: Provide a Redis/Memcached adapter here for production
|
|
17
|
+
cache?: IdentityCache;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class IMAGXP {
|
|
21
|
+
private publisher: IMAGXPPublisher;
|
|
22
|
+
private origin: ContentOrigin;
|
|
23
|
+
private ready: Promise<void>;
|
|
24
|
+
|
|
25
|
+
private constructor(config: IMAGXPConfig) {
|
|
26
|
+
this.publisher = new IMAGXPPublisher(
|
|
27
|
+
{ version: '1.1', ...config.policy } as AccessPolicy,
|
|
28
|
+
config.strategy || 'PASSIVE',
|
|
29
|
+
config.cache
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
this.origin = ContentOrigin[config.meta.origin];
|
|
33
|
+
|
|
34
|
+
this.ready = generateKeyPair().then(keys => {
|
|
35
|
+
return this.publisher.initialize(keys);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static init(config: IMAGXPConfig): IMAGXP {
|
|
40
|
+
return new IMAGXP(config);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Express Middleware
|
|
45
|
+
*/
|
|
46
|
+
middleware() {
|
|
47
|
+
return async (req: any, res: any, next: any) => {
|
|
48
|
+
await this.ready;
|
|
49
|
+
|
|
50
|
+
// Normalize headers to lowercase dictionary
|
|
51
|
+
const headers: Record<string, string> = {};
|
|
52
|
+
Object.keys(req.headers).forEach(key => {
|
|
53
|
+
headers[key.toLowerCase()] = req.headers[key] as string;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Retrieve Raw Payload if available (optional but good for crypto)
|
|
57
|
+
// Note: Express body parsing might interfere, so we usually rely on the header content.
|
|
58
|
+
const rawPayload = headers['x-imagxp-payload'];
|
|
59
|
+
|
|
60
|
+
// Evaluate Visitor
|
|
61
|
+
const result = await this.publisher.evaluateVisitor(headers, rawPayload);
|
|
62
|
+
|
|
63
|
+
// Enforce Decision
|
|
64
|
+
if (!result.allowed) {
|
|
65
|
+
res.status(result.status).json({
|
|
66
|
+
error: result.reason,
|
|
67
|
+
visitor_type: result.visitorType,
|
|
68
|
+
proof_used: result.proofUsed
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Inject Provenance Headers (For the humans/agents that got through)
|
|
74
|
+
const respHeaders = await this.publisher.generateResponseHeaders(this.origin);
|
|
75
|
+
Object.entries(respHeaders).forEach(([k, v]) => {
|
|
76
|
+
res.setHeader(k, v);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Attach metadata to request for downstream use
|
|
80
|
+
req.aamp = {
|
|
81
|
+
verified: result.visitorType === 'VERIFIED_AGENT',
|
|
82
|
+
type: result.visitorType,
|
|
83
|
+
...result.metadata
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
next();
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
discoveryHandler() {
|
|
91
|
+
return (req: any, res: any) => {
|
|
92
|
+
res.setHeader('Content-Type', 'application/json');
|
|
93
|
+
res.send(JSON.stringify(this.publisher.getPolicy(), null, 2));
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
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 } from './crypto.js';
|
|
8
|
+
|
|
9
|
+
type NextRequest = any;
|
|
10
|
+
type NextResponse = any;
|
|
11
|
+
|
|
12
|
+
const createJsonResponse = (body: any, status = 200) => {
|
|
13
|
+
return new Response(JSON.stringify(body), {
|
|
14
|
+
status,
|
|
15
|
+
headers: { 'Content-Type': 'application/json' }
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface IMAGXPConfig {
|
|
20
|
+
policy: Omit<AccessPolicy, 'version'>;
|
|
21
|
+
meta: {
|
|
22
|
+
origin: keyof typeof ContentOrigin;
|
|
23
|
+
paymentPointer?: string;
|
|
24
|
+
};
|
|
25
|
+
strategy?: UnauthenticatedStrategy;
|
|
26
|
+
// Optional: Provide a KV/Redis adapter here for production
|
|
27
|
+
cache?: IdentityCache;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class IMAGXPNext {
|
|
31
|
+
private publisher: IMAGXPPublisher;
|
|
32
|
+
private origin: ContentOrigin;
|
|
33
|
+
private ready: Promise<void>;
|
|
34
|
+
|
|
35
|
+
private constructor(config: IMAGXPConfig) {
|
|
36
|
+
this.publisher = new IMAGXPPublisher(
|
|
37
|
+
{ version: '1.1', ...config.policy } as AccessPolicy,
|
|
38
|
+
config.strategy || 'PASSIVE',
|
|
39
|
+
config.cache
|
|
40
|
+
);
|
|
41
|
+
this.origin = ContentOrigin[config.meta.origin];
|
|
42
|
+
this.ready = generateKeyPair().then(keys => this.publisher.initialize(keys));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static init(config: IMAGXPConfig): IMAGXPNext {
|
|
46
|
+
return new IMAGXPNext(config);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Serverless Route Wrapper
|
|
52
|
+
*/
|
|
53
|
+
withProtection(handler: (req: NextRequest) => Promise<NextResponse>) {
|
|
54
|
+
return async (req: NextRequest) => {
|
|
55
|
+
await this.ready;
|
|
56
|
+
|
|
57
|
+
// Extract Headers map
|
|
58
|
+
const headers: Record<string, string> = {};
|
|
59
|
+
req.headers.forEach((value: string, key: string) => {
|
|
60
|
+
headers[key.toLowerCase()] = value;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Evaluate
|
|
64
|
+
const result = await this.publisher.evaluateVisitor(headers, headers['x-imagxp-payload']);
|
|
65
|
+
|
|
66
|
+
if (!result.allowed) {
|
|
67
|
+
return createJsonResponse({
|
|
68
|
+
error: result.reason,
|
|
69
|
+
visitor_type: result.visitorType,
|
|
70
|
+
proof_used: result.proofUsed
|
|
71
|
+
}, result.status);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Execute Handler
|
|
75
|
+
const response = await handler(req);
|
|
76
|
+
|
|
77
|
+
// Inject Provenance
|
|
78
|
+
const imagxpHeaders = await this.publisher.generateResponseHeaders(this.origin);
|
|
79
|
+
if (response && response.headers) {
|
|
80
|
+
Object.entries(imagxpHeaders).forEach(([k, v]) => {
|
|
81
|
+
response.headers.set(k, v);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return response;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
discoveryHandler() {
|
|
90
|
+
return async () => {
|
|
91
|
+
return createJsonResponse(this.publisher.getPolicy());
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
package/src/proof.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
}
|