@aamp/protocol 1.1.0 → 1.1.2

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/src/nextjs.ts CHANGED
@@ -1,109 +1,109 @@
1
- /**
2
- * Layer 3: Framework Adapters
3
- * Serverless integration for Next.js (App Router & API Routes).
4
- */
5
- import { AAMPPublisher } from './publisher';
6
- import { AccessPolicy, ContentOrigin, SignedAccessRequest } from './types';
7
- import { generateKeyPair } from './crypto';
8
- import { HEADERS } from './constants';
9
-
10
- type NextRequest = any;
11
- type NextResponse = any;
12
-
13
- const createJsonResponse = (body: any, status = 200) => {
14
- return new Response(JSON.stringify(body), {
15
- status,
16
- headers: { 'Content-Type': 'application/json' }
17
- });
18
- };
19
-
20
- export interface AAMPConfig {
21
- policy: Omit<AccessPolicy, 'version'>;
22
- meta: {
23
- origin: keyof typeof ContentOrigin;
24
- paymentPointer?: string;
25
- };
26
- }
27
-
28
- export class AAMPNext {
29
- private publisher: AAMPPublisher;
30
- private origin: ContentOrigin;
31
- private ready: Promise<void>;
32
-
33
- private constructor(config: AAMPConfig) {
34
- this.publisher = new AAMPPublisher({
35
- version: '1.1',
36
- ...config.policy
37
- } as AccessPolicy);
38
- this.origin = ContentOrigin[config.meta.origin];
39
-
40
- this.ready = generateKeyPair().then(keys => this.publisher.initialize(keys));
41
- }
42
-
43
- static init(config: AAMPConfig): AAMPNext {
44
- return new AAMPNext(config);
45
- }
46
-
47
- /**
48
- * Serverless Route Wrapper
49
- */
50
- withProtection(handler: (req: NextRequest) => Promise<NextResponse>) {
51
- return async (req: NextRequest) => {
52
- await this.ready;
53
-
54
- // 1. Active Verification
55
- const payloadHeader = req.headers.get(HEADERS.PAYLOAD);
56
- const sigHeader = req.headers.get(HEADERS.SIGNATURE);
57
- const keyHeader = req.headers.get(HEADERS.PUBLIC_KEY);
58
-
59
- if (payloadHeader && sigHeader && keyHeader) {
60
- try {
61
- const headerJson = atob(payloadHeader); // RAW STRING
62
- const requestHeader = JSON.parse(headerJson);
63
-
64
- const signedRequest: SignedAccessRequest = {
65
- header: requestHeader,
66
- signature: sigHeader,
67
- publicKey: keyHeader
68
- };
69
-
70
- const agentKey = await crypto.subtle.importKey(
71
- "spki",
72
- new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))),
73
- { name: "ECDSA", namedCurve: "P-256" },
74
- true,
75
- ["verify"]
76
- );
77
-
78
- // Pass raw headerJson to ensure signature matches exactly what was signed
79
- const result = await this.publisher.verifyRequest(signedRequest, agentKey, headerJson);
80
-
81
- if (!result.allowed) {
82
- return createJsonResponse({ error: result.reason }, 403);
83
- }
84
- } catch (e) {
85
- return createJsonResponse({ error: "Invalid AAMP Signature" }, 400);
86
- }
87
- }
88
-
89
- // 2. Execute Handler
90
- const response = await handler(req);
91
-
92
- // 3. Inject Provenance Headers (Passive)
93
- const aampHeaders = await this.publisher.generateResponseHeaders(this.origin);
94
- if (response && response.headers) {
95
- Object.entries(aampHeaders).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
- }
1
+ /**
2
+ * Layer 3: Framework Adapters
3
+ * Serverless integration for Next.js (App Router & API Routes).
4
+ */
5
+ import { AAMPPublisher } from './publisher';
6
+ import { AccessPolicy, ContentOrigin, SignedAccessRequest } from './types';
7
+ import { generateKeyPair } from './crypto';
8
+ import { HEADERS } from './constants';
9
+
10
+ type NextRequest = any;
11
+ type NextResponse = any;
12
+
13
+ const createJsonResponse = (body: any, status = 200) => {
14
+ return new Response(JSON.stringify(body), {
15
+ status,
16
+ headers: { 'Content-Type': 'application/json' }
17
+ });
18
+ };
19
+
20
+ export interface AAMPConfig {
21
+ policy: Omit<AccessPolicy, 'version'>;
22
+ meta: {
23
+ origin: keyof typeof ContentOrigin;
24
+ paymentPointer?: string;
25
+ };
26
+ }
27
+
28
+ export class AAMPNext {
29
+ private publisher: AAMPPublisher;
30
+ private origin: ContentOrigin;
31
+ private ready: Promise<void>;
32
+
33
+ private constructor(config: AAMPConfig) {
34
+ this.publisher = new AAMPPublisher({
35
+ version: '1.1',
36
+ ...config.policy
37
+ } as AccessPolicy);
38
+ this.origin = ContentOrigin[config.meta.origin];
39
+
40
+ this.ready = generateKeyPair().then(keys => this.publisher.initialize(keys));
41
+ }
42
+
43
+ static init(config: AAMPConfig): AAMPNext {
44
+ return new AAMPNext(config);
45
+ }
46
+
47
+ /**
48
+ * Serverless Route Wrapper
49
+ */
50
+ withProtection(handler: (req: NextRequest) => Promise<NextResponse>) {
51
+ return async (req: NextRequest) => {
52
+ await this.ready;
53
+
54
+ // 1. Active Verification
55
+ const payloadHeader = req.headers.get(HEADERS.PAYLOAD);
56
+ const sigHeader = req.headers.get(HEADERS.SIGNATURE);
57
+ const keyHeader = req.headers.get(HEADERS.PUBLIC_KEY);
58
+
59
+ if (payloadHeader && sigHeader && keyHeader) {
60
+ try {
61
+ const headerJson = atob(payloadHeader); // RAW STRING
62
+ const requestHeader = JSON.parse(headerJson);
63
+
64
+ const signedRequest: SignedAccessRequest = {
65
+ header: requestHeader,
66
+ signature: sigHeader,
67
+ publicKey: keyHeader
68
+ };
69
+
70
+ const agentKey = await crypto.subtle.importKey(
71
+ "spki",
72
+ new Uint8Array(atob(keyHeader).split('').map(c => c.charCodeAt(0))),
73
+ { name: "ECDSA", namedCurve: "P-256" },
74
+ true,
75
+ ["verify"]
76
+ );
77
+
78
+ // Pass raw headerJson to ensure signature matches exactly what was signed
79
+ const result = await this.publisher.verifyRequest(signedRequest, agentKey, headerJson);
80
+
81
+ if (!result.allowed) {
82
+ return createJsonResponse({ error: result.reason }, 403);
83
+ }
84
+ } catch (e) {
85
+ return createJsonResponse({ error: "Invalid AAMP Signature" }, 400);
86
+ }
87
+ }
88
+
89
+ // 2. Execute Handler
90
+ const response = await handler(req);
91
+
92
+ // 3. Inject Provenance Headers (Passive)
93
+ const aampHeaders = await this.publisher.generateResponseHeaders(this.origin);
94
+ if (response && response.headers) {
95
+ Object.entries(aampHeaders).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
109
  }
package/src/publisher.ts CHANGED
@@ -1,107 +1,107 @@
1
- /**
2
- * Layer 2: Publisher Middleware
3
- * Used by content owners to enforce policy and log access.
4
- */
5
- import { AccessPolicy, AccessPurpose, SignedAccessRequest, ContentOrigin, FeedbackSignal } from './types';
6
- import { verifySignature, signData } from './crypto';
7
- import { MAX_CLOCK_SKEW_MS, HEADERS } from './constants';
8
-
9
- export interface VerificationResult {
10
- allowed: boolean;
11
- reason: string;
12
- responseHeaders?: Record<string, string>;
13
- }
14
-
15
- export class AAMPPublisher {
16
- private policy: AccessPolicy;
17
- private keyPair: CryptoKeyPair | null = null;
18
-
19
- constructor(policy: AccessPolicy) {
20
- this.policy = policy;
21
- }
22
-
23
- // Publishers now need keys too, to sign their Content Origin declarations
24
- async initialize(keyPair: CryptoKeyPair) {
25
- this.keyPair = keyPair;
26
- }
27
-
28
- getPolicy(): AccessPolicy {
29
- return this.policy;
30
- }
31
-
32
- /**
33
- * Verifies an incoming AI access request.
34
- *
35
- * @param request The parsed request object
36
- * @param requestPublicKey The agent's public key
37
- * @param rawPayload (Optional) The raw string received over the wire. REQUIRED for robust verification.
38
- */
39
- async verifyRequest(
40
- request: SignedAccessRequest,
41
- requestPublicKey: CryptoKey,
42
- rawPayload?: string
43
- ): Promise<VerificationResult> {
44
-
45
- // 1. Replay Attack Prevention (Timestamp Check)
46
- const requestTime = new Date(request.header.ts).getTime();
47
- const now = Date.now();
48
- if (Math.abs(now - requestTime) > MAX_CLOCK_SKEW_MS) {
49
- return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew exceeded.' };
50
- }
51
-
52
- // 2. Policy Enforcement (Usage Type)
53
- // STRICT CHECK: Training
54
- if (request.header.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
55
- return { allowed: false, reason: 'POLICY_DENIED: Training not allowed by site owner.' };
56
- }
57
- // STRICT CHECK: RAG / Retrieval
58
- if (request.header.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
59
- return { allowed: false, reason: 'POLICY_DENIED: RAG Retrieval not allowed.' };
60
- }
61
-
62
- // 3. Economic Policy Enforcement
63
- if (this.policy.requiresPayment) {
64
- const isExemptViaAds = this.policy.allowAdSupportedAccess && request.header.context.ads_displayed;
65
-
66
- if (!isExemptViaAds) {
67
- return {
68
- allowed: false,
69
- reason: 'PAYMENT_REQUIRED: Site requires payment or ad-supported access.'
70
- };
71
- }
72
- }
73
-
74
- // 4. Cryptographic Verification (The "Proof")
75
- // CRITICAL: We prefer the rawPayload if available to avoid JSON parsing/stringify mismatches.
76
- const signableString = rawPayload || JSON.stringify(request.header);
77
-
78
- const isValid = await verifySignature(requestPublicKey, signableString, request.signature);
79
-
80
- if (!isValid) {
81
- return { allowed: false, reason: 'CRYPTO_FAIL: Signature verification failed.' };
82
- }
83
-
84
- return { allowed: true, reason: 'OK' };
85
- }
86
-
87
- /**
88
- * Verifies a Feedback Signal (Spam Report) from an Agent.
89
- * Part of the AAMP Immune System.
90
- */
91
- async verifyFeedback(signal: FeedbackSignal, signature: string, agentPublicKey: CryptoKey): Promise<boolean> {
92
- const signableString = JSON.stringify(signal);
93
- return await verifySignature(agentPublicKey, signableString, signature);
94
- }
95
-
96
- async generateResponseHeaders(origin: ContentOrigin): Promise<Record<string, string>> {
97
- if (!this.keyPair) throw new Error("Publisher keys not initialized");
98
-
99
- const payload = JSON.stringify({ origin, ts: Date.now() });
100
- const signature = await signData(this.keyPair.privateKey, payload);
101
-
102
- return {
103
- [HEADERS.CONTENT_ORIGIN]: origin,
104
- [HEADERS.PROVENANCE_SIG]: signature
105
- };
106
- }
1
+ /**
2
+ * Layer 2: Publisher Middleware
3
+ * Used by content owners to enforce policy and log access.
4
+ */
5
+ import { AccessPolicy, AccessPurpose, SignedAccessRequest, ContentOrigin, FeedbackSignal } from './types';
6
+ import { verifySignature, signData } from './crypto';
7
+ import { MAX_CLOCK_SKEW_MS, HEADERS } from './constants';
8
+
9
+ export interface VerificationResult {
10
+ allowed: boolean;
11
+ reason: string;
12
+ responseHeaders?: Record<string, string>;
13
+ }
14
+
15
+ export class AAMPPublisher {
16
+ private policy: AccessPolicy;
17
+ private keyPair: CryptoKeyPair | null = null;
18
+
19
+ constructor(policy: AccessPolicy) {
20
+ this.policy = policy;
21
+ }
22
+
23
+ // Publishers now need keys too, to sign their Content Origin declarations
24
+ async initialize(keyPair: CryptoKeyPair) {
25
+ this.keyPair = keyPair;
26
+ }
27
+
28
+ getPolicy(): AccessPolicy {
29
+ return this.policy;
30
+ }
31
+
32
+ /**
33
+ * Verifies an incoming AI access request.
34
+ *
35
+ * @param request The parsed request object
36
+ * @param requestPublicKey The agent's public key
37
+ * @param rawPayload (Optional) The raw string received over the wire. REQUIRED for robust verification.
38
+ */
39
+ async verifyRequest(
40
+ request: SignedAccessRequest,
41
+ requestPublicKey: CryptoKey,
42
+ rawPayload?: string
43
+ ): Promise<VerificationResult> {
44
+
45
+ // 1. Replay Attack Prevention (Timestamp Check)
46
+ const requestTime = new Date(request.header.ts).getTime();
47
+ const now = Date.now();
48
+ if (Math.abs(now - requestTime) > MAX_CLOCK_SKEW_MS) {
49
+ return { allowed: false, reason: 'TIMESTAMP_INVALID: Clock skew exceeded.' };
50
+ }
51
+
52
+ // 2. Policy Enforcement (Usage Type)
53
+ // STRICT CHECK: Training
54
+ if (request.header.purpose === AccessPurpose.CRAWL_TRAINING && !this.policy.allowTraining) {
55
+ return { allowed: false, reason: 'POLICY_DENIED: Training not allowed by site owner.' };
56
+ }
57
+ // STRICT CHECK: RAG / Retrieval
58
+ if (request.header.purpose === AccessPurpose.RAG_RETRIEVAL && !this.policy.allowRAG) {
59
+ return { allowed: false, reason: 'POLICY_DENIED: RAG Retrieval not allowed.' };
60
+ }
61
+
62
+ // 3. Economic Policy Enforcement
63
+ if (this.policy.requiresPayment) {
64
+ const isExemptViaAds = this.policy.allowAdSupportedAccess && request.header.context.ads_displayed;
65
+
66
+ if (!isExemptViaAds) {
67
+ return {
68
+ allowed: false,
69
+ reason: 'PAYMENT_REQUIRED: Site requires payment or ad-supported access.'
70
+ };
71
+ }
72
+ }
73
+
74
+ // 4. Cryptographic Verification (The "Proof")
75
+ // CRITICAL: We prefer the rawPayload if available to avoid JSON parsing/stringify mismatches.
76
+ const signableString = rawPayload || JSON.stringify(request.header);
77
+
78
+ const isValid = await verifySignature(requestPublicKey, signableString, request.signature);
79
+
80
+ if (!isValid) {
81
+ return { allowed: false, reason: 'CRYPTO_FAIL: Signature verification failed.' };
82
+ }
83
+
84
+ return { allowed: true, reason: 'OK' };
85
+ }
86
+
87
+ /**
88
+ * Verifies a Feedback Signal (Spam Report) from an Agent.
89
+ * Part of the AAMP Immune System.
90
+ */
91
+ async verifyFeedback(signal: FeedbackSignal, signature: string, agentPublicKey: CryptoKey): Promise<boolean> {
92
+ const signableString = JSON.stringify(signal);
93
+ return await verifySignature(agentPublicKey, signableString, signature);
94
+ }
95
+
96
+ async generateResponseHeaders(origin: ContentOrigin): Promise<Record<string, string>> {
97
+ if (!this.keyPair) throw new Error("Publisher keys not initialized");
98
+
99
+ const payload = JSON.stringify({ origin, ts: Date.now() });
100
+ const signature = await signData(this.keyPair.privateKey, payload);
101
+
102
+ return {
103
+ [HEADERS.CONTENT_ORIGIN]: origin,
104
+ [HEADERS.PROVENANCE_SIG]: signature
105
+ };
106
+ }
107
107
  }
package/src/types.ts CHANGED
@@ -1,98 +1,98 @@
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
- * Optional Rate Limiting (The Speed Limit)
29
- * Defines technical boundaries for the handshake.
30
- */
31
- export interface RateLimitConfig {
32
- requestsPerMinute: number;
33
- tokensPerMinute?: number;
34
- }
35
-
36
- /**
37
- * Optional Monetization (The Settlement Layer)
38
- *
39
- * AAMP is neutral. Settlement can happen via:
40
- * 1. BROKER: A 3rd party clearing house (e.g., "AI-AdSense").
41
- * 2. CRYPTO: Direct on-chain settlement.
42
- * 3. TREATY: A private legal contract signed offline (Enterprise).
43
- */
44
- export interface MonetizationConfig {
45
- method: 'BROKER' | 'CRYPTO' | 'PRIVATE_TREATY';
46
- /**
47
- * The destination for settlement.
48
- * - If BROKER: The API URL of the clearing house.
49
- * - If CRYPTO: The wallet address.
50
- * - If TREATY: The Contract ID or "Contact Sales".
51
- */
52
- location: string;
53
- }
54
-
55
- export interface AccessPolicy {
56
- version: '1.1';
57
- allowTraining: boolean;
58
- allowRAG: boolean;
59
- attributionRequired: boolean;
60
-
61
- // Economic Signals
62
- allowAdSupportedAccess: boolean; // If true, Agents showing ads are exempt from payment
63
- requiresPayment: boolean; // If true, Access is denied unless Ad-Supported condition is met
64
- paymentPointer?: string; // @deprecated (Legacy support)
65
-
66
- // V1.1: Optional Traffic Control
67
- rateLimit?: RateLimitConfig;
68
-
69
- // V1.1: Optional Settlement Info
70
- // If undefined, parties are assumed to have no economic relationship or settled offline.
71
- monetization?: MonetizationConfig;
72
- }
73
-
74
- export interface ProtocolHeader {
75
- v: '1.1';
76
- ts: string;
77
- agent_id: string;
78
- resource: string;
79
- purpose: AccessPurpose;
80
- // Access Context
81
- context: {
82
- ads_displayed: boolean; // Is the AI Agent displaying ads alongside this content?
83
- };
84
- }
85
-
86
- export interface SignedAccessRequest {
87
- header: ProtocolHeader;
88
- signature: string;
89
- publicKey?: string;
90
- }
91
-
92
- export interface FeedbackSignal {
93
- target_resource: string;
94
- agent_id: string;
95
- quality_score: number; // 0.0 to 1.0
96
- flags: QualityFlag[];
97
- timestamp: string;
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
+ * Optional Rate Limiting (The Speed Limit)
29
+ * Defines technical boundaries for the handshake.
30
+ */
31
+ export interface RateLimitConfig {
32
+ requestsPerMinute: number;
33
+ tokensPerMinute?: number;
34
+ }
35
+
36
+ /**
37
+ * Optional Monetization (The Settlement Layer)
38
+ *
39
+ * AAMP is neutral. Settlement can happen via:
40
+ * 1. BROKER: A 3rd party clearing house (e.g., "AI-AdSense").
41
+ * 2. CRYPTO: Direct on-chain settlement.
42
+ * 3. TREATY: A private legal contract signed offline (Enterprise).
43
+ */
44
+ export interface MonetizationConfig {
45
+ method: 'BROKER' | 'CRYPTO' | 'PRIVATE_TREATY';
46
+ /**
47
+ * The destination for settlement.
48
+ * - If BROKER: The API URL of the clearing house.
49
+ * - If CRYPTO: The wallet address.
50
+ * - If TREATY: The Contract ID or "Contact Sales".
51
+ */
52
+ location: string;
53
+ }
54
+
55
+ export interface AccessPolicy {
56
+ version: '1.1';
57
+ allowTraining: boolean;
58
+ allowRAG: boolean;
59
+ attributionRequired: boolean;
60
+
61
+ // Economic Signals
62
+ allowAdSupportedAccess: boolean; // If true, Agents showing ads are exempt from payment
63
+ requiresPayment: boolean; // If true, Access is denied unless Ad-Supported condition is met
64
+ paymentPointer?: string; // @deprecated (Legacy support)
65
+
66
+ // V1.1: Optional Traffic Control
67
+ rateLimit?: RateLimitConfig;
68
+
69
+ // V1.1: Optional Settlement Info
70
+ // If undefined, parties are assumed to have no economic relationship or settled offline.
71
+ monetization?: MonetizationConfig;
72
+ }
73
+
74
+ export interface ProtocolHeader {
75
+ v: '1.1';
76
+ ts: string;
77
+ agent_id: string;
78
+ resource: string;
79
+ purpose: AccessPurpose;
80
+ // Access Context
81
+ context: {
82
+ ads_displayed: boolean; // Is the AI Agent displaying ads alongside this content?
83
+ };
84
+ }
85
+
86
+ export interface SignedAccessRequest {
87
+ header: ProtocolHeader;
88
+ signature: string;
89
+ publicKey?: string;
90
+ }
91
+
92
+ export interface FeedbackSignal {
93
+ target_resource: string;
94
+ agent_id: string;
95
+ quality_score: number; // 0.0 to 1.0
96
+ flags: QualityFlag[];
97
+ timestamp: string;
98
98
  }