@aamp/protocol 1.1.2 → 1.1.4
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/dist/agent.d.ts +7 -2
- package/dist/agent.js +15 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +4 -1
- package/dist/express.d.ts +3 -1
- package/dist/express.js +28 -36
- package/dist/nextjs.d.ts +3 -1
- package/dist/nextjs.js +15 -30
- package/dist/publisher.d.ts +19 -20
- package/dist/publisher.js +210 -41
- package/dist/types.d.ts +30 -17
- package/package.json +23 -23
- package/src/agent.ts +87 -71
- package/src/constants.ts +38 -34
- package/src/crypto.ts +68 -68
- package/src/express.ts +94 -103
- package/src/index.ts +12 -12
- package/src/nextjs.ts +91 -108
- package/src/publisher.ts +306 -106
- package/src/types.ts +112 -97
- package/test/handshake.spec.ts +62 -66
- package/tsconfig.json +14 -14
package/dist/types.d.ts
CHANGED
|
@@ -21,31 +21,37 @@ export declare enum QualityFlag {
|
|
|
21
21
|
HIGH_QUALITY = "HIGH_QUALITY"
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* DNS Identity Manifest
|
|
25
|
+
* Hosted at: https://{agent_id}/.well-known/aamp-agent.json
|
|
26
26
|
*/
|
|
27
|
-
export interface
|
|
28
|
-
|
|
29
|
-
|
|
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>;
|
|
30
39
|
}
|
|
31
40
|
/**
|
|
32
41
|
* Optional Monetization (The Settlement Layer)
|
|
33
|
-
*
|
|
34
|
-
* AAMP is neutral. Settlement can happen via:
|
|
35
|
-
* 1. BROKER: A 3rd party clearing house (e.g., "AI-AdSense").
|
|
36
|
-
* 2. CRYPTO: Direct on-chain settlement.
|
|
37
|
-
* 3. TREATY: A private legal contract signed offline (Enterprise).
|
|
38
42
|
*/
|
|
39
43
|
export interface MonetizationConfig {
|
|
40
44
|
method: 'BROKER' | 'CRYPTO' | 'PRIVATE_TREATY';
|
|
41
|
-
/**
|
|
42
|
-
* The destination for settlement.
|
|
43
|
-
* - If BROKER: The API URL of the clearing house.
|
|
44
|
-
* - If CRYPTO: The wallet address.
|
|
45
|
-
* - If TREATY: The Contract ID or "Contact Sales".
|
|
46
|
-
*/
|
|
47
45
|
location: string;
|
|
48
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Handling Non-AAMP Visitors
|
|
49
|
+
*
|
|
50
|
+
* PASSIVE: Allow everyone (Legacy web behavior).
|
|
51
|
+
* HYBRID: Allow verified Agents AND likely Humans (Browser Heuristics). Block bots.
|
|
52
|
+
* STRICT: Allow ONLY verified AAMP Agents. (API Mode).
|
|
53
|
+
*/
|
|
54
|
+
export type UnauthenticatedStrategy = 'PASSIVE' | 'HYBRID' | 'STRICT';
|
|
49
55
|
export interface AccessPolicy {
|
|
50
56
|
version: '1.1';
|
|
51
57
|
allowTraining: boolean;
|
|
@@ -54,7 +60,7 @@ export interface AccessPolicy {
|
|
|
54
60
|
allowAdSupportedAccess: boolean;
|
|
55
61
|
requiresPayment: boolean;
|
|
56
62
|
paymentPointer?: string;
|
|
57
|
-
|
|
63
|
+
requireIdentityBinding?: boolean;
|
|
58
64
|
monetization?: MonetizationConfig;
|
|
59
65
|
}
|
|
60
66
|
export interface ProtocolHeader {
|
|
@@ -79,3 +85,10 @@ export interface FeedbackSignal {
|
|
|
79
85
|
flags: QualityFlag[];
|
|
80
86
|
timestamp: string;
|
|
81
87
|
}
|
|
88
|
+
export interface EvaluationResult {
|
|
89
|
+
allowed: boolean;
|
|
90
|
+
status: 200 | 400 | 401 | 403;
|
|
91
|
+
reason: string;
|
|
92
|
+
visitorType: 'VERIFIED_AGENT' | 'LIKELY_HUMAN' | 'UNIDENTIFIED_BOT';
|
|
93
|
+
metadata?: any;
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@aamp/protocol",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "TypeScript reference implementation of AAMP",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"build": "tsc",
|
|
9
|
-
"test": "ts-node test/handshake.spec.ts",
|
|
10
|
-
"prepublishOnly": "npm run test && npm run build"
|
|
11
|
-
},
|
|
12
|
-
"repository": {
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "https://github.com/aamp-protocol/aamp.git"
|
|
15
|
-
},
|
|
16
|
-
"publishConfig": {
|
|
17
|
-
"access": "public"
|
|
18
|
-
},
|
|
19
|
-
"devDependencies": {
|
|
20
|
-
"typescript": "^5.0.0",
|
|
21
|
-
"ts-node": "^10.9.0",
|
|
22
|
-
"@types/node": "^20.0.0"
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@aamp/protocol",
|
|
3
|
+
"version": "1.1.4",
|
|
4
|
+
"description": "TypeScript reference implementation of AAMP v1.1",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "ts-node test/handshake.spec.ts",
|
|
10
|
+
"prepublishOnly": "npm run test && npm run build"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/aamp-protocol/aamp.git"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.0.0",
|
|
21
|
+
"ts-node": "^10.9.0",
|
|
22
|
+
"@types/node": "^20.0.0"
|
|
23
|
+
}
|
|
24
24
|
}
|
package/src/agent.ts
CHANGED
|
@@ -1,72 +1,88 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 2: Agent SDK
|
|
3
|
-
*/
|
|
4
|
-
import { AccessPurpose, ProtocolHeader, SignedAccessRequest, FeedbackSignal, QualityFlag } from './types';
|
|
5
|
-
import { generateKeyPair, signData, exportPublicKey } from './crypto';
|
|
6
|
-
import { AAMP_VERSION } from './constants';
|
|
7
|
-
|
|
8
|
-
export interface AccessOptions {
|
|
9
|
-
adsDisplayed?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class AAMPAgent {
|
|
13
|
-
private keyPair: CryptoKeyPair | null = null;
|
|
14
|
-
public agentId: string = "pending";
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Initialize the Agent Identity (Ephemeral or Persisted)
|
|
18
|
-
* @param customAgentId
|
|
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: AAMP_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
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*/
|
|
54
|
-
async
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Layer 2: Agent SDK
|
|
3
|
+
*/
|
|
4
|
+
import { AccessPurpose, ProtocolHeader, SignedAccessRequest, FeedbackSignal, QualityFlag, AgentIdentityManifest } from './types';
|
|
5
|
+
import { generateKeyPair, signData, exportPublicKey } from './crypto';
|
|
6
|
+
import { AAMP_VERSION } from './constants';
|
|
7
|
+
|
|
8
|
+
export interface AccessOptions {
|
|
9
|
+
adsDisplayed?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class AAMPAgent {
|
|
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: AAMP_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/aamp-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
|
+
}
|
|
72
88
|
}
|
package/src/constants.ts
CHANGED
|
@@ -1,35 +1,39 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Layer 1: Protocol Constants
|
|
3
|
-
* These values are immutable and defined by the AAMP Specification.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const AAMP_VERSION = '1.1';
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// Transport: The
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Layer 1: Protocol Constants
|
|
3
|
+
* These values are immutable and defined by the AAMP Specification.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const AAMP_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/aamp-agent.json
|
|
10
|
+
export const WELL_KNOWN_AGENT_PATH = '/.well-known/aamp-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-aamp-payload',
|
|
16
|
+
// Transport: The cryptographic signature (Hex)
|
|
17
|
+
SIGNATURE: 'x-aamp-signature',
|
|
18
|
+
// Transport: The Agent's Public Key (Base64 SPKI)
|
|
19
|
+
PUBLIC_KEY: 'x-aamp-public-key',
|
|
20
|
+
|
|
21
|
+
// Informational / Legacy (Optional if Payload is present)
|
|
22
|
+
AGENT_ID: 'x-aamp-agent-id',
|
|
23
|
+
TIMESTAMP: 'x-aamp-timestamp',
|
|
24
|
+
ALGORITHM: 'x-aamp-alg',
|
|
25
|
+
|
|
26
|
+
// v1.1 Addition: Provenance (Server to Agent)
|
|
27
|
+
CONTENT_ORIGIN: 'x-aamp-content-origin',
|
|
28
|
+
PROVENANCE_SIG: 'x-aamp-provenance-sig'
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
// Cryptographic Settings
|
|
32
|
+
export const CRYPTO_CONFIG = {
|
|
33
|
+
ALGORITHM_NAME: 'ECDSA',
|
|
34
|
+
CURVE: 'P-256',
|
|
35
|
+
HASH: 'SHA-256',
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
// Tolerance
|
|
35
39
|
export const MAX_CLOCK_SKEW_MS = 5 * 60 * 1000; // 5 minutes
|
package/src/crypto.ts
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
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
|
-
return await crypto.subtle.verify(
|
|
32
|
-
{ name: "ECDSA", hash: { name: "SHA-256" } },
|
|
33
|
-
publicKey,
|
|
34
|
-
signatureBytes as any,
|
|
35
|
-
encodedData as any
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function exportPublicKey(key: CryptoKey): Promise<string> {
|
|
40
|
-
const exported = await crypto.subtle.exportKey("spki", key);
|
|
41
|
-
return btoa(String.fromCharCode(...new Uint8Array(exported)));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function importPublicKey(keyData: string): Promise<CryptoKey> {
|
|
45
|
-
const binaryString = atob(keyData);
|
|
46
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
47
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
48
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return await crypto.subtle.importKey(
|
|
52
|
-
"spki",
|
|
53
|
-
bytes,
|
|
54
|
-
{ name: "ECDSA", namedCurve: "P-256" },
|
|
55
|
-
true,
|
|
56
|
-
["verify"]
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Helpers
|
|
61
|
-
function bufToHex(buffer: ArrayBuffer): string {
|
|
62
|
-
return Array.from(new Uint8Array(buffer))
|
|
63
|
-
.map(b => b.toString(16).padStart(2, '0'))
|
|
64
|
-
.join('');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function hexToBuf(hex: string): Uint8Array {
|
|
68
|
-
return new Uint8Array(hex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
|
|
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
|
+
return await crypto.subtle.verify(
|
|
32
|
+
{ name: "ECDSA", hash: { name: "SHA-256" } },
|
|
33
|
+
publicKey,
|
|
34
|
+
signatureBytes as any,
|
|
35
|
+
encodedData as any
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function exportPublicKey(key: CryptoKey): Promise<string> {
|
|
40
|
+
const exported = await crypto.subtle.exportKey("spki", key);
|
|
41
|
+
return btoa(String.fromCharCode(...new Uint8Array(exported)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function importPublicKey(keyData: string): Promise<CryptoKey> {
|
|
45
|
+
const binaryString = atob(keyData);
|
|
46
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
47
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
48
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return await crypto.subtle.importKey(
|
|
52
|
+
"spki",
|
|
53
|
+
bytes,
|
|
54
|
+
{ name: "ECDSA", namedCurve: "P-256" },
|
|
55
|
+
true,
|
|
56
|
+
["verify"]
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Helpers
|
|
61
|
+
function bufToHex(buffer: ArrayBuffer): string {
|
|
62
|
+
return Array.from(new Uint8Array(buffer))
|
|
63
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
64
|
+
.join('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hexToBuf(hex: string): Uint8Array {
|
|
68
|
+
return new Uint8Array(hex.match(/.{1,2}/g)!.map(byte => parseInt(byte, 16)));
|
|
69
69
|
}
|