@aimarket/agent 0.1.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 +59 -0
- package/dist/agent.d.ts +90 -0
- package/dist/agent.js +302 -0
- package/dist/eip712.d.ts +45 -0
- package/dist/eip712.js +41 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +32 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/models.d.ts +83 -0
- package/dist/models.js +20 -0
- package/dist/signer.d.ts +76 -0
- package/dist/signer.js +164 -0
- package/dist/tee.d.ts +54 -0
- package/dist/tee.js +148 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @aimarket/agent
|
|
2
|
+
|
|
3
|
+
AI Market Protocol v2 consumer SDK for **TypeScript** — Electron, Node.js servers, and web apps.
|
|
4
|
+
|
|
5
|
+
Discover, pay for, and invoke AI capabilities from the decentralized marketplace, with production cryptography:
|
|
6
|
+
|
|
7
|
+
- **Ed25519** (`ed25519:<base64>`) for canonical hub / invoke signatures
|
|
8
|
+
- **EIP-712** (`keccak256` + secp256k1, `eip712:0x<r><s><v>`) for on-chain channel debits
|
|
9
|
+
|
|
10
|
+
Part of the [AIMarket SDKs](https://github.com/alexar76/aimarket-sdks) (Dart · TypeScript · Rust) — all three ship the same version and the same model shapes, enforced by an ecosystem parity guard in CI.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Today — build from source (the SDK lives in typescript/):
|
|
16
|
+
git clone https://github.com/alexar76/aimarket-sdks
|
|
17
|
+
cd aimarket-sdks/typescript && npm install && npm run build
|
|
18
|
+
# …then reference it via `npm link` or a `file:` dependency.
|
|
19
|
+
|
|
20
|
+
# Once published: npm install @aimarket/agent (npm registry — not live yet)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { AimarketAgent } from '@aimarket/agent';
|
|
27
|
+
|
|
28
|
+
const agent = new AimarketAgent({
|
|
29
|
+
hubUrl: 'https://hub.aicom.io',
|
|
30
|
+
walletKey: loadYourWalletKey(),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Discover capabilities for an intent — returns a ranked PlanStep[].
|
|
34
|
+
const plan = await agent.discover({
|
|
35
|
+
intent: 'ATS scoring rules for fintech roles',
|
|
36
|
+
budget: 1.0,
|
|
37
|
+
limit: 5,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Open a $5 channel (good for ~50 calls), then invoke the best match.
|
|
41
|
+
const channel = await agent.openChannel(5.0);
|
|
42
|
+
const result = await agent.invoke({
|
|
43
|
+
capabilityId: plan[0].capability.capability_id,
|
|
44
|
+
input: { target_role: 'Senior PM', industry: 'fintech' },
|
|
45
|
+
channelId: channel.channel_id,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log('Output:', result.output, '· cost $', result.price_usd, '· TEE', result.tee_verified);
|
|
49
|
+
await agent.closeChannel(channel.channel_id);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Links
|
|
53
|
+
|
|
54
|
+
- Ecosystem & live demos: <https://alexar76.github.io/aicom/>
|
|
55
|
+
- Protocol spec & schemas: <https://github.com/alexar76/aimarket-protocol>
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Market Protocol v2 Consumer Agent (TypeScript).
|
|
3
|
+
*
|
|
4
|
+
* Implements the 5-phase consumer cycle:
|
|
5
|
+
* 1. Discovery — fetch well-known + search
|
|
6
|
+
* 2. Channel — open pre-funded payment channel
|
|
7
|
+
* 3. Invoke — call capability with payment header
|
|
8
|
+
* 4. Settle — close channel, get refund
|
|
9
|
+
* 5. Verify — TEE attestation check
|
|
10
|
+
*
|
|
11
|
+
* Target: Electron desktop apps, Node.js servers, web apps.
|
|
12
|
+
*/
|
|
13
|
+
import { type BillOfMaterials, type Channel, type InvokeResult, type PlanStep, type Settlement, type TeeAttestation, type TeeReceipt } from './models';
|
|
14
|
+
/** Configuration for [AimarketAgent] behavior. */
|
|
15
|
+
export interface AimarketAgentConfig {
|
|
16
|
+
hubUrl: string;
|
|
17
|
+
walletKey: string;
|
|
18
|
+
affiliate: string;
|
|
19
|
+
timeoutMs: number;
|
|
20
|
+
maxRetries: number;
|
|
21
|
+
trustedCodeHashes?: Record<string, string>;
|
|
22
|
+
verifyTee: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface AimarketAgentOptions {
|
|
25
|
+
hubUrl: string;
|
|
26
|
+
walletKey: string;
|
|
27
|
+
affiliate?: string;
|
|
28
|
+
trustedCodeHashes?: Record<string, string>;
|
|
29
|
+
timeoutMs?: number;
|
|
30
|
+
maxRetries?: number;
|
|
31
|
+
verifyTee?: boolean;
|
|
32
|
+
/** Injectable fetch for tests. */
|
|
33
|
+
fetch?: typeof fetch;
|
|
34
|
+
}
|
|
35
|
+
export declare class AimarketAgent {
|
|
36
|
+
private readonly config;
|
|
37
|
+
private readonly signer;
|
|
38
|
+
private readonly teeVerifier;
|
|
39
|
+
private readonly fetchFn;
|
|
40
|
+
private readonly channelCache;
|
|
41
|
+
private wellKnownCache;
|
|
42
|
+
constructor(opts: AimarketAgentOptions);
|
|
43
|
+
private retryWithBackoff;
|
|
44
|
+
private fetchWithTimeout;
|
|
45
|
+
wellKnown(): Promise<string>;
|
|
46
|
+
discover(opts: {
|
|
47
|
+
intent: string;
|
|
48
|
+
budget?: number;
|
|
49
|
+
limit?: number;
|
|
50
|
+
category?: string;
|
|
51
|
+
}): Promise<PlanStep[]>;
|
|
52
|
+
discoverProduct(productId: string): Promise<PlanStep[]>;
|
|
53
|
+
openChannel(depositUsd: number, token?: string, chain?: string): Promise<Channel>;
|
|
54
|
+
getChannelBalance(channelId: string): Promise<number>;
|
|
55
|
+
invoke(opts: {
|
|
56
|
+
capabilityId: string;
|
|
57
|
+
input: Record<string, unknown>;
|
|
58
|
+
channelId: string;
|
|
59
|
+
productId?: string;
|
|
60
|
+
sourceHub?: string;
|
|
61
|
+
verifyTee?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* TEE attestation for the target capability, obtained out-of-band (e.g.
|
|
64
|
+
* from the hub manifest or a prior invocation's `tee_attestation`). When
|
|
65
|
+
* supplied and TEE verification is enabled, it is verified BEFORE any input
|
|
66
|
+
* is transmitted; an invalid attestation aborts the call so sensitive input
|
|
67
|
+
* never reaches an unverified enclave.
|
|
68
|
+
*/
|
|
69
|
+
attestation?: TeeAttestation;
|
|
70
|
+
}): Promise<InvokeResult>;
|
|
71
|
+
private invokeOnce;
|
|
72
|
+
invokeBatch(opts: {
|
|
73
|
+
capabilityIds: string[];
|
|
74
|
+
inputs: Array<Record<string, unknown>>;
|
|
75
|
+
channelId: string;
|
|
76
|
+
sourceHub?: string;
|
|
77
|
+
}): Promise<InvokeResult[]>;
|
|
78
|
+
closeChannel(channelId: string): Promise<Settlement>;
|
|
79
|
+
verifyTeeAttestation(attestation: TeeAttestation, capabilityId: string): boolean;
|
|
80
|
+
verifyTeeReceipt(receipt: TeeReceipt, sentInput: string, receivedOutput: string): boolean;
|
|
81
|
+
trustCodeHash(capabilityId: string, codeHash: string): void;
|
|
82
|
+
fetchTrustedHashes(): Promise<number>;
|
|
83
|
+
runOnce(opts: {
|
|
84
|
+
intent: string;
|
|
85
|
+
input: Record<string, unknown>;
|
|
86
|
+
depositUsd?: number;
|
|
87
|
+
category?: string;
|
|
88
|
+
}): Promise<BillOfMaterials>;
|
|
89
|
+
dispose(): void;
|
|
90
|
+
}
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Market Protocol v2 Consumer Agent (TypeScript).
|
|
3
|
+
*
|
|
4
|
+
* Implements the 5-phase consumer cycle:
|
|
5
|
+
* 1. Discovery — fetch well-known + search
|
|
6
|
+
* 2. Channel — open pre-funded payment channel
|
|
7
|
+
* 3. Invoke — call capability with payment header
|
|
8
|
+
* 4. Settle — close channel, get refund
|
|
9
|
+
* 5. Verify — TEE attestation check
|
|
10
|
+
*
|
|
11
|
+
* Target: Electron desktop apps, Node.js servers, web apps.
|
|
12
|
+
*/
|
|
13
|
+
import { AimarketException, AimarketNetworkException, AimarketPaymentException, AimarketSafetyException, } from './errors';
|
|
14
|
+
import { channelBalanceRatio, channelIsExpired, } from './models';
|
|
15
|
+
import { MarketSigner } from './signer';
|
|
16
|
+
import { TeeVerifier } from './tee';
|
|
17
|
+
class CachedChannel {
|
|
18
|
+
channel;
|
|
19
|
+
cachedAt;
|
|
20
|
+
constructor(channel, cachedAt) {
|
|
21
|
+
this.channel = channel;
|
|
22
|
+
this.cachedAt = cachedAt;
|
|
23
|
+
}
|
|
24
|
+
get hasSufficientBalance() {
|
|
25
|
+
return channelBalanceRatio(this.channel) > 0.5;
|
|
26
|
+
}
|
|
27
|
+
get isNotExpired() {
|
|
28
|
+
return !channelIsExpired(this.channel);
|
|
29
|
+
}
|
|
30
|
+
get isReusable() {
|
|
31
|
+
return this.isNotExpired && this.hasSufficientBalance;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export class AimarketAgent {
|
|
35
|
+
config;
|
|
36
|
+
signer;
|
|
37
|
+
teeVerifier;
|
|
38
|
+
fetchFn;
|
|
39
|
+
channelCache = new Map();
|
|
40
|
+
wellKnownCache = null;
|
|
41
|
+
constructor(opts) {
|
|
42
|
+
this.config = {
|
|
43
|
+
hubUrl: opts.hubUrl.replace(/\/$/, ''),
|
|
44
|
+
walletKey: opts.walletKey,
|
|
45
|
+
affiliate: opts.affiliate ?? 'aimarket-sdk-ts',
|
|
46
|
+
timeoutMs: opts.timeoutMs ?? 30_000,
|
|
47
|
+
maxRetries: opts.maxRetries ?? 3,
|
|
48
|
+
trustedCodeHashes: opts.trustedCodeHashes,
|
|
49
|
+
verifyTee: opts.verifyTee ?? true,
|
|
50
|
+
};
|
|
51
|
+
this.signer = new MarketSigner(this.config.walletKey);
|
|
52
|
+
this.teeVerifier = new TeeVerifier({
|
|
53
|
+
signer: this.signer,
|
|
54
|
+
trustedCodeHashes: this.config.trustedCodeHashes,
|
|
55
|
+
});
|
|
56
|
+
this.fetchFn = opts.fetch ?? fetch;
|
|
57
|
+
}
|
|
58
|
+
async retryWithBackoff(operation) {
|
|
59
|
+
let lastError = new AimarketNetworkException(`Request failed after ${this.config.maxRetries} retries`);
|
|
60
|
+
for (let attempt = 0; attempt <= this.config.maxRetries; attempt++) {
|
|
61
|
+
try {
|
|
62
|
+
return await operation();
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (e instanceof AimarketNetworkException) {
|
|
66
|
+
lastError = e;
|
|
67
|
+
}
|
|
68
|
+
else if (e instanceof Error && e.name === 'AbortError') {
|
|
69
|
+
lastError = new AimarketNetworkException(`Request timed out: ${e.message}`);
|
|
70
|
+
}
|
|
71
|
+
else if (e instanceof TypeError ||
|
|
72
|
+
(e instanceof Error && e.message.includes('fetch'))) {
|
|
73
|
+
lastError = new AimarketNetworkException(`Network error: ${e.message}`);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (attempt < this.config.maxRetries) {
|
|
80
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** attempt));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
throw lastError;
|
|
84
|
+
}
|
|
85
|
+
async fetchWithTimeout(input, init) {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
|
|
88
|
+
try {
|
|
89
|
+
return await this.fetchFn(input, { ...init, signal: controller.signal });
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// ── Phase 1: Discovery ────────────────────────────────────────
|
|
96
|
+
async wellKnown() {
|
|
97
|
+
if (this.wellKnownCache)
|
|
98
|
+
return this.wellKnownCache;
|
|
99
|
+
const resp = await this.fetchWithTimeout(`${this.config.hubUrl}/.well-known/ai-market.json`);
|
|
100
|
+
if (!resp.ok) {
|
|
101
|
+
throw new AimarketException(`Failed to fetch well-known: ${resp.status}`);
|
|
102
|
+
}
|
|
103
|
+
this.wellKnownCache = await resp.text();
|
|
104
|
+
return this.wellKnownCache;
|
|
105
|
+
}
|
|
106
|
+
async discover(opts) {
|
|
107
|
+
const params = new URLSearchParams({
|
|
108
|
+
intent: opts.intent,
|
|
109
|
+
limit: String(opts.limit ?? 5),
|
|
110
|
+
});
|
|
111
|
+
if (opts.budget !== undefined)
|
|
112
|
+
params.set('budget_usd', String(opts.budget));
|
|
113
|
+
if (opts.category)
|
|
114
|
+
params.set('category', opts.category);
|
|
115
|
+
const resp = await this.fetchWithTimeout(`${this.config.hubUrl}/ai-market/v2/search?${params}`, { headers: { 'X-AIMarket-Affiliate': this.config.affiliate } });
|
|
116
|
+
if (!resp.ok) {
|
|
117
|
+
throw new AimarketException(`Discovery failed: ${resp.status} ${await resp.text()}`);
|
|
118
|
+
}
|
|
119
|
+
const data = await resp.json();
|
|
120
|
+
return data.results;
|
|
121
|
+
}
|
|
122
|
+
async discoverProduct(productId) {
|
|
123
|
+
return this.discover({ intent: `product:${productId}` });
|
|
124
|
+
}
|
|
125
|
+
// ── Phase 2: Channel Open ─────────────────────────────────────
|
|
126
|
+
async openChannel(depositUsd, token = 'USDT', chain = 'base') {
|
|
127
|
+
const cacheKey = `${depositUsd}:${token}:${chain}`;
|
|
128
|
+
const cached = this.channelCache.get(cacheKey);
|
|
129
|
+
if (cached?.isReusable) {
|
|
130
|
+
return cached.channel;
|
|
131
|
+
}
|
|
132
|
+
if (cached) {
|
|
133
|
+
this.channelCache.delete(cacheKey);
|
|
134
|
+
}
|
|
135
|
+
const resp = await this.fetchWithTimeout(`${this.config.hubUrl}/ai-market/v2/channel/open`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
'X-AIMarket-Affiliate': this.config.affiliate,
|
|
140
|
+
},
|
|
141
|
+
body: JSON.stringify({ deposit_usd: depositUsd, token, chain }),
|
|
142
|
+
});
|
|
143
|
+
if (resp.status === 404) {
|
|
144
|
+
throw new AimarketException('Payment channels not available on this hub');
|
|
145
|
+
}
|
|
146
|
+
if (resp.status !== 200 && resp.status !== 201) {
|
|
147
|
+
throw new AimarketException(`Channel open failed: ${resp.status} ${await resp.text()}`);
|
|
148
|
+
}
|
|
149
|
+
// The hub wraps the channel in a `{ channel: {...} }` envelope (matching the
|
|
150
|
+
// Python agent + live hub); unwrap it, tolerating a bare object for forward-compat.
|
|
151
|
+
const raw = await resp.json();
|
|
152
|
+
const channel = raw && raw.channel ? raw.channel : raw;
|
|
153
|
+
this.channelCache.set(cacheKey, new CachedChannel(channel, new Date()));
|
|
154
|
+
return channel;
|
|
155
|
+
}
|
|
156
|
+
async getChannelBalance(channelId) {
|
|
157
|
+
const resp = await this.fetchWithTimeout(`${this.config.hubUrl}/ai-market/v2/channel/${channelId}`, { headers: { 'X-AIMarket-Affiliate': this.config.affiliate } });
|
|
158
|
+
if (!resp.ok) {
|
|
159
|
+
throw new AimarketException(`Failed to get channel balance: ${resp.status}`);
|
|
160
|
+
}
|
|
161
|
+
const data = (await resp.json());
|
|
162
|
+
return data.balance_usd ?? 0;
|
|
163
|
+
}
|
|
164
|
+
// ── Phase 3: Invoke ───────────────────────────────────────────
|
|
165
|
+
async invoke(opts) {
|
|
166
|
+
return this.retryWithBackoff(() => this.invokeOnce(opts));
|
|
167
|
+
}
|
|
168
|
+
async invokeOnce(opts) {
|
|
169
|
+
const verifyTee = opts.verifyTee ?? true;
|
|
170
|
+
if (verifyTee && this.config.verifyTee && opts.attestation) {
|
|
171
|
+
// Phase 5 pre-check: verify the enclave attestation BEFORE sending input,
|
|
172
|
+
// so user data never reaches a capability whose code hash / signature
|
|
173
|
+
// can't be trusted. Fail closed — a bad attestation aborts the call.
|
|
174
|
+
const verdict = this.teeVerifier.verifyAttestationDetailed(opts.attestation, opts.capabilityId);
|
|
175
|
+
if (!verdict.isValid) {
|
|
176
|
+
throw new AimarketSafetyException(`TEE attestation verification failed for ${opts.capabilityId}: ` +
|
|
177
|
+
verdict.failures.join('; '));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const headers = this.signer.signedHeaders({
|
|
181
|
+
channelId: opts.channelId,
|
|
182
|
+
capabilityId: opts.capabilityId,
|
|
183
|
+
affiliate: this.config.affiliate,
|
|
184
|
+
});
|
|
185
|
+
headers['Content-Type'] = 'application/json';
|
|
186
|
+
const body = {
|
|
187
|
+
capability_id: opts.capabilityId,
|
|
188
|
+
input: opts.input,
|
|
189
|
+
};
|
|
190
|
+
if (opts.productId)
|
|
191
|
+
body.product_id = opts.productId;
|
|
192
|
+
if (opts.sourceHub)
|
|
193
|
+
body.source_hub = opts.sourceHub;
|
|
194
|
+
const startMs = performance.now();
|
|
195
|
+
const resp = await this.fetchWithTimeout(`${this.config.hubUrl}/ai-market/v2/invoke`, {
|
|
196
|
+
method: 'POST',
|
|
197
|
+
headers,
|
|
198
|
+
body: JSON.stringify(body),
|
|
199
|
+
});
|
|
200
|
+
const latencyMs = performance.now() - startMs;
|
|
201
|
+
if (resp.status === 403) {
|
|
202
|
+
const data = (await resp.json());
|
|
203
|
+
throw new AimarketSafetyException(data.reason ?? 'Blocked by safety gate');
|
|
204
|
+
}
|
|
205
|
+
if (resp.status === 402) {
|
|
206
|
+
throw new AimarketPaymentException('Channel depleted or expired — open a new channel');
|
|
207
|
+
}
|
|
208
|
+
if (!resp.ok) {
|
|
209
|
+
return {
|
|
210
|
+
success: false,
|
|
211
|
+
price_usd: 0,
|
|
212
|
+
latency_ms: latencyMs,
|
|
213
|
+
safety_blocked: false,
|
|
214
|
+
tee_verified: false,
|
|
215
|
+
error: `HTTP ${resp.status}: ${await resp.text()}`,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return resp.json();
|
|
219
|
+
}
|
|
220
|
+
async invokeBatch(opts) {
|
|
221
|
+
if (opts.capabilityIds.length !== opts.inputs.length) {
|
|
222
|
+
throw new Error('capabilityIds and inputs must have the same length');
|
|
223
|
+
}
|
|
224
|
+
return Promise.all(opts.capabilityIds.map((capabilityId, i) => this.invoke({
|
|
225
|
+
capabilityId,
|
|
226
|
+
input: opts.inputs[i],
|
|
227
|
+
channelId: opts.channelId,
|
|
228
|
+
sourceHub: opts.sourceHub,
|
|
229
|
+
})));
|
|
230
|
+
}
|
|
231
|
+
// ── Phase 4: Settle ───────────────────────────────────────────
|
|
232
|
+
async closeChannel(channelId) {
|
|
233
|
+
for (const [key, cached] of this.channelCache) {
|
|
234
|
+
if (cached.channel.channel_id === channelId) {
|
|
235
|
+
this.channelCache.delete(key);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const resp = await this.fetchWithTimeout(`${this.config.hubUrl}/ai-market/v2/channel/close`, {
|
|
240
|
+
method: 'POST',
|
|
241
|
+
headers: {
|
|
242
|
+
'Content-Type': 'application/json',
|
|
243
|
+
'X-AIMarket-Affiliate': this.config.affiliate,
|
|
244
|
+
},
|
|
245
|
+
body: JSON.stringify({ channel_id: channelId }),
|
|
246
|
+
});
|
|
247
|
+
if (resp.status === 404) {
|
|
248
|
+
throw new AimarketException(`Channel not found: ${channelId}`);
|
|
249
|
+
}
|
|
250
|
+
if (!resp.ok) {
|
|
251
|
+
throw new AimarketException(`Settlement failed: ${resp.status} ${await resp.text()}`);
|
|
252
|
+
}
|
|
253
|
+
return resp.json();
|
|
254
|
+
}
|
|
255
|
+
// ── Phase 5: Verify ───────────────────────────────────────────
|
|
256
|
+
verifyTeeAttestation(attestation, capabilityId) {
|
|
257
|
+
return this.teeVerifier.verifyAttestation(attestation, capabilityId);
|
|
258
|
+
}
|
|
259
|
+
verifyTeeReceipt(receipt, sentInput, receivedOutput) {
|
|
260
|
+
return this.teeVerifier.verifyReceipt(receipt, sentInput, receivedOutput);
|
|
261
|
+
}
|
|
262
|
+
trustCodeHash(capabilityId, codeHash) {
|
|
263
|
+
this.teeVerifier.trustCodeHash(capabilityId, codeHash);
|
|
264
|
+
}
|
|
265
|
+
async fetchTrustedHashes() {
|
|
266
|
+
return this.teeVerifier.fetchTrustedHashes(this.config.hubUrl, this.fetchFn);
|
|
267
|
+
}
|
|
268
|
+
// ── Full cycle ────────────────────────────────────────────────
|
|
269
|
+
async runOnce(opts) {
|
|
270
|
+
const depositUsd = opts.depositUsd ?? 5.0;
|
|
271
|
+
const plan = await this.discover({
|
|
272
|
+
intent: opts.intent,
|
|
273
|
+
budget: depositUsd,
|
|
274
|
+
category: opts.category,
|
|
275
|
+
});
|
|
276
|
+
if (plan.length === 0) {
|
|
277
|
+
throw new AimarketException(`No capabilities found for: ${opts.intent}`);
|
|
278
|
+
}
|
|
279
|
+
const channel = await this.openChannel(depositUsd);
|
|
280
|
+
const step = plan[0];
|
|
281
|
+
const result = await this.invoke({
|
|
282
|
+
capabilityId: step.capability.capability_id,
|
|
283
|
+
input: opts.input,
|
|
284
|
+
channelId: channel.channel_id,
|
|
285
|
+
productId: step.capability.product_id,
|
|
286
|
+
sourceHub: step.capability.source_hub,
|
|
287
|
+
});
|
|
288
|
+
const settlement = await this.closeChannel(channel.channel_id);
|
|
289
|
+
return {
|
|
290
|
+
task: opts.intent,
|
|
291
|
+
plan,
|
|
292
|
+
results: [result],
|
|
293
|
+
settlement,
|
|
294
|
+
total_spent_usd: result.price_usd,
|
|
295
|
+
protocol_version: 'v2',
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
dispose() {
|
|
299
|
+
this.channelCache.clear();
|
|
300
|
+
this.wellKnownCache = null;
|
|
301
|
+
}
|
|
302
|
+
}
|
package/dist/eip712.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production EIP-712 encoding for AIMarketEscrow debit authorizations.
|
|
3
|
+
*
|
|
4
|
+
* Matches `contracts/evm/src/AIMarketEscrow.sol` and OpenZeppelin
|
|
5
|
+
* `MessageHashUtils.toTypedDataHash`.
|
|
6
|
+
*/
|
|
7
|
+
import { type Address, type Hex } from 'viem';
|
|
8
|
+
export declare const EIP712_DOMAIN_TYPE = "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)";
|
|
9
|
+
export declare const DEBIT_AUTHORIZATION_TYPES: {
|
|
10
|
+
readonly DebitAuthorization: readonly [{
|
|
11
|
+
readonly name: "channelId";
|
|
12
|
+
readonly type: "bytes32";
|
|
13
|
+
}, {
|
|
14
|
+
readonly name: "hub";
|
|
15
|
+
readonly type: "address";
|
|
16
|
+
}, {
|
|
17
|
+
readonly name: "token";
|
|
18
|
+
readonly type: "address";
|
|
19
|
+
}, {
|
|
20
|
+
readonly name: "amount";
|
|
21
|
+
readonly type: "uint256";
|
|
22
|
+
}, {
|
|
23
|
+
readonly name: "receiptId";
|
|
24
|
+
readonly type: "bytes32";
|
|
25
|
+
}, {
|
|
26
|
+
readonly name: "nonce";
|
|
27
|
+
readonly type: "uint256";
|
|
28
|
+
}, {
|
|
29
|
+
readonly name: "deadline";
|
|
30
|
+
readonly type: "uint256";
|
|
31
|
+
}];
|
|
32
|
+
};
|
|
33
|
+
export interface DebitDigestParams {
|
|
34
|
+
channelId: Hex;
|
|
35
|
+
hub: Address;
|
|
36
|
+
token: Address;
|
|
37
|
+
amount: bigint;
|
|
38
|
+
receiptId: Hex;
|
|
39
|
+
nonce: bigint;
|
|
40
|
+
deadline: bigint;
|
|
41
|
+
chainId: number;
|
|
42
|
+
verifyingContract: Address;
|
|
43
|
+
}
|
|
44
|
+
/** Compute the EIP-712 digest a depositor signs for `debitChannel`. */
|
|
45
|
+
export declare function computeDebitDigest(params: DebitDigestParams): Hex;
|
package/dist/eip712.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production EIP-712 encoding for AIMarketEscrow debit authorizations.
|
|
3
|
+
*
|
|
4
|
+
* Matches `contracts/evm/src/AIMarketEscrow.sol` and OpenZeppelin
|
|
5
|
+
* `MessageHashUtils.toTypedDataHash`.
|
|
6
|
+
*/
|
|
7
|
+
import { hashTypedData } from 'viem';
|
|
8
|
+
export const EIP712_DOMAIN_TYPE = 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)';
|
|
9
|
+
export const DEBIT_AUTHORIZATION_TYPES = {
|
|
10
|
+
DebitAuthorization: [
|
|
11
|
+
{ name: 'channelId', type: 'bytes32' },
|
|
12
|
+
{ name: 'hub', type: 'address' },
|
|
13
|
+
{ name: 'token', type: 'address' },
|
|
14
|
+
{ name: 'amount', type: 'uint256' },
|
|
15
|
+
{ name: 'receiptId', type: 'bytes32' },
|
|
16
|
+
{ name: 'nonce', type: 'uint256' },
|
|
17
|
+
{ name: 'deadline', type: 'uint256' },
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
/** Compute the EIP-712 digest a depositor signs for `debitChannel`. */
|
|
21
|
+
export function computeDebitDigest(params) {
|
|
22
|
+
return hashTypedData({
|
|
23
|
+
domain: {
|
|
24
|
+
name: 'AIMarketEscrow',
|
|
25
|
+
version: '1',
|
|
26
|
+
chainId: params.chainId,
|
|
27
|
+
verifyingContract: params.verifyingContract,
|
|
28
|
+
},
|
|
29
|
+
types: DEBIT_AUTHORIZATION_TYPES,
|
|
30
|
+
primaryType: 'DebitAuthorization',
|
|
31
|
+
message: {
|
|
32
|
+
channelId: params.channelId,
|
|
33
|
+
hub: params.hub,
|
|
34
|
+
token: params.token,
|
|
35
|
+
amount: params.amount,
|
|
36
|
+
receiptId: params.receiptId,
|
|
37
|
+
nonce: params.nonce,
|
|
38
|
+
deadline: params.deadline,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Base exception for AI Market Protocol errors. */
|
|
2
|
+
export declare class AimarketException extends Error {
|
|
3
|
+
readonly statusCode?: number;
|
|
4
|
+
constructor(message: string, statusCode?: number);
|
|
5
|
+
}
|
|
6
|
+
/** Network-level error: timeout, connection refused, DNS failure, etc. */
|
|
7
|
+
export declare class AimarketNetworkException extends AimarketException {
|
|
8
|
+
constructor(message: string, statusCode?: number);
|
|
9
|
+
}
|
|
10
|
+
/** Payment failure: depleted channel, insufficient funds, or expired credit. */
|
|
11
|
+
export declare class AimarketPaymentException extends AimarketException {
|
|
12
|
+
constructor(message: string, statusCode?: number);
|
|
13
|
+
}
|
|
14
|
+
/** Safety gate blocked the invocation. */
|
|
15
|
+
export declare class AimarketSafetyException extends AimarketException {
|
|
16
|
+
readonly reason: string;
|
|
17
|
+
constructor(reason: string);
|
|
18
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Base exception for AI Market Protocol errors. */
|
|
2
|
+
export class AimarketException extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
constructor(message, statusCode) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'AimarketException';
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** Network-level error: timeout, connection refused, DNS failure, etc. */
|
|
11
|
+
export class AimarketNetworkException extends AimarketException {
|
|
12
|
+
constructor(message, statusCode) {
|
|
13
|
+
super(message, statusCode);
|
|
14
|
+
this.name = 'AimarketNetworkException';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Payment failure: depleted channel, insufficient funds, or expired credit. */
|
|
18
|
+
export class AimarketPaymentException extends AimarketException {
|
|
19
|
+
constructor(message, statusCode = 402) {
|
|
20
|
+
super(message, statusCode);
|
|
21
|
+
this.name = 'AimarketPaymentException';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Safety gate blocked the invocation. */
|
|
25
|
+
export class AimarketSafetyException extends AimarketException {
|
|
26
|
+
reason;
|
|
27
|
+
constructor(reason) {
|
|
28
|
+
super(`Safety blocked: ${reason}`, 403);
|
|
29
|
+
this.name = 'AimarketSafetyException';
|
|
30
|
+
this.reason = reason;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { AimarketAgent } from './agent';
|
|
2
|
+
export type { AimarketAgentConfig, AimarketAgentOptions } from './agent';
|
|
3
|
+
export { AimarketException, AimarketNetworkException, AimarketPaymentException, AimarketSafetyException, } from './errors';
|
|
4
|
+
export type { Capability, Channel, InvokeResult, PlanStep, Settlement, BillOfMaterials, TeeAttestation, TeeReceipt, SearchResponse, } from './models';
|
|
5
|
+
export { channelBalanceRatio, channelIsExpired, attestationCanonical, } from './models';
|
|
6
|
+
export { MarketSigner, computeDebitDigest, DEBIT_TYPEHASH_HEADER, ESCROW_CONTRACT_NAME, ESCROW_CONTRACT_VERSION, } from './signer';
|
|
7
|
+
export type { MarketSignerOptions } from './signer';
|
|
8
|
+
export type { DebitAuthorizationParams, Eip712Domain, TypedData, } from './signer';
|
|
9
|
+
export { TeePlatform, TeeVerifier, TrustedHashCache, teeVerificationPass, teeVerificationFail, } from './tee';
|
|
10
|
+
export type { TeeVerificationResult } from './tee';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AimarketAgent } from './agent';
|
|
2
|
+
export { AimarketException, AimarketNetworkException, AimarketPaymentException, AimarketSafetyException, } from './errors';
|
|
3
|
+
export { channelBalanceRatio, channelIsExpired, attestationCanonical, } from './models';
|
|
4
|
+
export { MarketSigner, computeDebitDigest, DEBIT_TYPEHASH_HEADER, ESCROW_CONTRACT_NAME, ESCROW_CONTRACT_VERSION, } from './signer';
|
|
5
|
+
export { TeePlatform, TeeVerifier, TrustedHashCache, teeVerificationPass, teeVerificationFail, } from './tee';
|
package/dist/models.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Data models for AI Market Protocol v2. */
|
|
2
|
+
export interface Capability {
|
|
3
|
+
capability_id: string;
|
|
4
|
+
product_id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
version: string;
|
|
7
|
+
description: string;
|
|
8
|
+
input_schema?: Record<string, unknown>;
|
|
9
|
+
output_schema?: Record<string, unknown>;
|
|
10
|
+
price_per_call_usd: number;
|
|
11
|
+
p50_latency_ms?: number;
|
|
12
|
+
success_rate_30d?: number;
|
|
13
|
+
source_hub: string;
|
|
14
|
+
source_hub_name?: string;
|
|
15
|
+
trust_score?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface Channel {
|
|
18
|
+
channel_id: string;
|
|
19
|
+
deposit_usd: number;
|
|
20
|
+
balance_usd: number;
|
|
21
|
+
token: string;
|
|
22
|
+
chain: string;
|
|
23
|
+
expires_at: string;
|
|
24
|
+
}
|
|
25
|
+
export interface TeeAttestation {
|
|
26
|
+
platform: string;
|
|
27
|
+
enclave_id: string;
|
|
28
|
+
code_hash: string;
|
|
29
|
+
pcr_values: Record<string, string>;
|
|
30
|
+
instance_id: string;
|
|
31
|
+
region: string;
|
|
32
|
+
timestamp: string;
|
|
33
|
+
ttl_s: number;
|
|
34
|
+
signature: string;
|
|
35
|
+
}
|
|
36
|
+
export interface TeeReceipt {
|
|
37
|
+
receipt_id: string;
|
|
38
|
+
input_hash: string;
|
|
39
|
+
output_hash: string;
|
|
40
|
+
signature: string;
|
|
41
|
+
}
|
|
42
|
+
export interface InvokeResult {
|
|
43
|
+
success: boolean;
|
|
44
|
+
output?: Record<string, unknown>;
|
|
45
|
+
price_usd: number;
|
|
46
|
+
latency_ms: number;
|
|
47
|
+
safety_blocked: boolean;
|
|
48
|
+
safety_reason?: string;
|
|
49
|
+
tee_verified: boolean;
|
|
50
|
+
tee_attestation?: TeeAttestation;
|
|
51
|
+
tee_receipt?: TeeReceipt;
|
|
52
|
+
error?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface PlanStep {
|
|
55
|
+
capability: Capability;
|
|
56
|
+
relevance_score: number;
|
|
57
|
+
rationale: string;
|
|
58
|
+
}
|
|
59
|
+
export interface Settlement {
|
|
60
|
+
channel_id: string;
|
|
61
|
+
total_spent_usd: number;
|
|
62
|
+
refund_usd: number;
|
|
63
|
+
invocations: number;
|
|
64
|
+
}
|
|
65
|
+
export interface BillOfMaterials {
|
|
66
|
+
task: string;
|
|
67
|
+
plan: PlanStep[];
|
|
68
|
+
results: InvokeResult[];
|
|
69
|
+
settlement?: Settlement;
|
|
70
|
+
total_spent_usd: number;
|
|
71
|
+
protocol_version: string;
|
|
72
|
+
}
|
|
73
|
+
export interface SearchResponse {
|
|
74
|
+
results: PlanStep[];
|
|
75
|
+
total: number;
|
|
76
|
+
hub: string;
|
|
77
|
+
}
|
|
78
|
+
/** Ratio of remaining balance to original deposit (0..1). */
|
|
79
|
+
export declare function channelBalanceRatio(channel: Channel): number;
|
|
80
|
+
/** Whether the channel has passed its expiry timestamp. */
|
|
81
|
+
export declare function channelIsExpired(channel: Channel): boolean;
|
|
82
|
+
/** Canonical string used for TEE attestation signature verification. */
|
|
83
|
+
export declare function attestationCanonical(att: TeeAttestation): string;
|
package/dist/models.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Data models for AI Market Protocol v2. */
|
|
2
|
+
/** Ratio of remaining balance to original deposit (0..1). */
|
|
3
|
+
export function channelBalanceRatio(channel) {
|
|
4
|
+
if (channel.deposit_usd <= 0)
|
|
5
|
+
return 0;
|
|
6
|
+
return channel.balance_usd / channel.deposit_usd;
|
|
7
|
+
}
|
|
8
|
+
/** Whether the channel has passed its expiry timestamp. */
|
|
9
|
+
export function channelIsExpired(channel) {
|
|
10
|
+
const ts = Date.parse(channel.expires_at);
|
|
11
|
+
if (Number.isNaN(ts))
|
|
12
|
+
return true;
|
|
13
|
+
return ts < Date.now();
|
|
14
|
+
}
|
|
15
|
+
/** Canonical string used for TEE attestation signature verification. */
|
|
16
|
+
export function attestationCanonical(att) {
|
|
17
|
+
return (`platform:${att.platform}|enclave_id:${att.enclave_id}|code_hash:${att.code_hash}` +
|
|
18
|
+
`|pcr0:${att.pcr_values.pcr0 ?? ''}|instance:${att.instance_id}` +
|
|
19
|
+
`|region:${att.region}|timestamp:${att.timestamp}|ttl:${att.ttl_s}`);
|
|
20
|
+
}
|
package/dist/signer.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production Ed25519 + EIP-712 signing for AI Market Protocol v2.
|
|
3
|
+
*
|
|
4
|
+
* - Canonical hub messages: real Ed25519 (`ed25519:<base64>`)
|
|
5
|
+
* - On-chain debit auth: keccak256 EIP-712 + secp256k1 (`eip712:0x<r><s><v>`)
|
|
6
|
+
*/
|
|
7
|
+
import { type Address, type Hex } from 'viem';
|
|
8
|
+
export interface Eip712Domain {
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
chainId: number;
|
|
12
|
+
verifyingContract: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TypedData {
|
|
15
|
+
domain: Eip712Domain;
|
|
16
|
+
primaryType: string;
|
|
17
|
+
message: Record<string, string | number | bigint>;
|
|
18
|
+
}
|
|
19
|
+
export declare const DEBIT_TYPEHASH_HEADER = "DebitAuthorization(bytes32 channelId,address hub,address token,uint256 amount,bytes32 receiptId,uint256 nonce,uint256 deadline)";
|
|
20
|
+
export declare const ESCROW_CONTRACT_NAME = "AIMarketEscrow";
|
|
21
|
+
export declare const ESCROW_CONTRACT_VERSION = "1";
|
|
22
|
+
export interface DebitAuthorizationParams {
|
|
23
|
+
channelId: string;
|
|
24
|
+
hub: string;
|
|
25
|
+
token: string;
|
|
26
|
+
amount: bigint;
|
|
27
|
+
receiptId: string;
|
|
28
|
+
nonce: bigint;
|
|
29
|
+
deadline: number;
|
|
30
|
+
chainId?: number;
|
|
31
|
+
verifyingContract?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface MarketSignerOptions {
|
|
34
|
+
/** 32-byte Ed25519 seed (64-char hex) for canonical invoke signatures. */
|
|
35
|
+
ed25519SeedHex: string;
|
|
36
|
+
/** secp256k1 Ethereum private key (64-char hex) for EIP-712 debit auth. */
|
|
37
|
+
ethereumPrivateKeyHex?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare class MarketSigner {
|
|
40
|
+
private readonly seed;
|
|
41
|
+
private readonly keyPair;
|
|
42
|
+
private readonly ethereumPrivateKeyHex?;
|
|
43
|
+
constructor(seedOrOptions: string | MarketSignerOptions, ethereumPrivateKeyHex?: string);
|
|
44
|
+
/** Ed25519 public key as base64 (hub-compatible). */
|
|
45
|
+
publicKeyBase64(): string;
|
|
46
|
+
/** Ed25519 public key as hex. */
|
|
47
|
+
publicKeyHex(): string;
|
|
48
|
+
/** Sign a canonical UTF-8 string → `ed25519:<base64>`. */
|
|
49
|
+
signCanonical(canonical: string): string;
|
|
50
|
+
/** Verify `ed25519:<base64>` with hex- or base64-encoded public key. */
|
|
51
|
+
verify(publicKey: string, signature: string, canonical: string): boolean;
|
|
52
|
+
/** Sign EIP-712 debit authorization → `eip712:0x<130 hex chars>`. */
|
|
53
|
+
signDebitAuthorization(params: DebitAuthorizationParams): string;
|
|
54
|
+
signEip712TypedData(_data: TypedData): string;
|
|
55
|
+
signedHeaders(args: {
|
|
56
|
+
channelId: string;
|
|
57
|
+
capabilityId: string;
|
|
58
|
+
affiliate: string;
|
|
59
|
+
}): Record<string, string>;
|
|
60
|
+
verifyHubSignature(hubPublicKey: string, message: string, signature: string): boolean;
|
|
61
|
+
static generateEd25519Keypair(): {
|
|
62
|
+
seedHex: string;
|
|
63
|
+
publicKeyBase64: string;
|
|
64
|
+
};
|
|
65
|
+
/** Generate a secp256k1 Ethereum wallet for channel deposits / EIP-712. */
|
|
66
|
+
static generateEthereumWallet(): {
|
|
67
|
+
privateKeyHex: Hex;
|
|
68
|
+
address: Address;
|
|
69
|
+
};
|
|
70
|
+
/** @deprecated Prefer [generateEd25519Keypair] or [generateEthereumWallet]. */
|
|
71
|
+
static generateWallet(): {
|
|
72
|
+
privateKey: string;
|
|
73
|
+
address: string;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export { computeDebitDigest } from './eip712';
|
package/dist/signer.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Production Ed25519 + EIP-712 signing for AI Market Protocol v2.
|
|
3
|
+
*
|
|
4
|
+
* - Canonical hub messages: real Ed25519 (`ed25519:<base64>`)
|
|
5
|
+
* - On-chain debit auth: keccak256 EIP-712 + secp256k1 (`eip712:0x<r><s><v>`)
|
|
6
|
+
*/
|
|
7
|
+
import nacl from 'tweetnacl';
|
|
8
|
+
import naclUtil from 'tweetnacl-util';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
10
|
+
import { secp256k1 } from '@noble/curves/secp256k1.js';
|
|
11
|
+
import { hexToBytes } from 'viem';
|
|
12
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
13
|
+
import { computeDebitDigest } from './eip712';
|
|
14
|
+
export const DEBIT_TYPEHASH_HEADER = 'DebitAuthorization(bytes32 channelId,address hub,address token,uint256 amount,bytes32 receiptId,uint256 nonce,uint256 deadline)';
|
|
15
|
+
export const ESCROW_CONTRACT_NAME = 'AIMarketEscrow';
|
|
16
|
+
export const ESCROW_CONTRACT_VERSION = '1';
|
|
17
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
18
|
+
function parseSeedHex(hex) {
|
|
19
|
+
const normalized = hex.replace(/^0x/i, '');
|
|
20
|
+
if (normalized.length === 64 && /^[0-9a-fA-F]+$/.test(normalized)) {
|
|
21
|
+
const bytes = new Uint8Array(32);
|
|
22
|
+
for (let i = 0; i < 32; i++) {
|
|
23
|
+
bytes[i] = parseInt(normalized.slice(i * 2, i * 2 + 2), 16);
|
|
24
|
+
}
|
|
25
|
+
return bytes;
|
|
26
|
+
}
|
|
27
|
+
// Dev fallback: hash arbitrary short strings to a 32-byte seed.
|
|
28
|
+
return createHash('sha256').update(hex, 'utf8').digest();
|
|
29
|
+
}
|
|
30
|
+
function parseEthPrivateKey(hex) {
|
|
31
|
+
const normalized = hex.replace(/^0x/i, '');
|
|
32
|
+
if (normalized.length !== 64 || !/^[0-9a-fA-F]+$/.test(normalized)) {
|
|
33
|
+
throw new Error('ethereumPrivateKeyHex must be a 32-byte hex string (64 chars)');
|
|
34
|
+
}
|
|
35
|
+
return `0x${normalized}`;
|
|
36
|
+
}
|
|
37
|
+
function decodePublicKey(publicKey) {
|
|
38
|
+
const trimmed = publicKey.trim();
|
|
39
|
+
if (/^[0-9a-fA-F]+$/.test(trimmed) && trimmed.length === 64) {
|
|
40
|
+
const bytes = new Uint8Array(32);
|
|
41
|
+
for (let i = 0; i < 32; i++) {
|
|
42
|
+
bytes[i] = parseInt(trimmed.slice(i * 2, i * 2 + 2), 16);
|
|
43
|
+
}
|
|
44
|
+
return bytes;
|
|
45
|
+
}
|
|
46
|
+
return naclUtil.decodeBase64(trimmed);
|
|
47
|
+
}
|
|
48
|
+
function decodeEd25519Signature(signature) {
|
|
49
|
+
const raw = signature.startsWith('ed25519:') ? signature.slice(8) : signature;
|
|
50
|
+
return naclUtil.decodeBase64(raw);
|
|
51
|
+
}
|
|
52
|
+
// ── Signer ──────────────────────────────────────────────────────────────────
|
|
53
|
+
export class MarketSigner {
|
|
54
|
+
seed;
|
|
55
|
+
keyPair;
|
|
56
|
+
ethereumPrivateKeyHex;
|
|
57
|
+
constructor(seedOrOptions, ethereumPrivateKeyHex) {
|
|
58
|
+
const opts = typeof seedOrOptions === 'string'
|
|
59
|
+
? { ed25519SeedHex: seedOrOptions, ethereumPrivateKeyHex }
|
|
60
|
+
: seedOrOptions;
|
|
61
|
+
this.seed = parseSeedHex(opts.ed25519SeedHex);
|
|
62
|
+
this.keyPair = nacl.sign.keyPair.fromSeed(this.seed);
|
|
63
|
+
this.ethereumPrivateKeyHex = opts.ethereumPrivateKeyHex
|
|
64
|
+
? parseEthPrivateKey(opts.ethereumPrivateKeyHex)
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
/** Ed25519 public key as base64 (hub-compatible). */
|
|
68
|
+
publicKeyBase64() {
|
|
69
|
+
return naclUtil.encodeBase64(this.keyPair.publicKey);
|
|
70
|
+
}
|
|
71
|
+
/** Ed25519 public key as hex. */
|
|
72
|
+
publicKeyHex() {
|
|
73
|
+
return Buffer.from(this.keyPair.publicKey).toString('hex');
|
|
74
|
+
}
|
|
75
|
+
/** Sign a canonical UTF-8 string → `ed25519:<base64>`. */
|
|
76
|
+
signCanonical(canonical) {
|
|
77
|
+
const message = new TextEncoder().encode(canonical);
|
|
78
|
+
const sig = nacl.sign.detached(message, this.keyPair.secretKey);
|
|
79
|
+
return `ed25519:${naclUtil.encodeBase64(sig)}`;
|
|
80
|
+
}
|
|
81
|
+
/** Verify `ed25519:<base64>` with hex- or base64-encoded public key. */
|
|
82
|
+
verify(publicKey, signature, canonical) {
|
|
83
|
+
if (!signature.startsWith('ed25519:'))
|
|
84
|
+
return false;
|
|
85
|
+
try {
|
|
86
|
+
const message = new TextEncoder().encode(canonical);
|
|
87
|
+
const sig = decodeEd25519Signature(signature);
|
|
88
|
+
const pub = decodePublicKey(publicKey);
|
|
89
|
+
return nacl.sign.detached.verify(message, sig, pub);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Sign EIP-712 debit authorization → `eip712:0x<130 hex chars>`. */
|
|
96
|
+
signDebitAuthorization(params) {
|
|
97
|
+
const ethKey = this.ethereumPrivateKeyHex;
|
|
98
|
+
if (!ethKey) {
|
|
99
|
+
throw new Error('ethereumPrivateKeyHex is required for EIP-712 debit signing. ' +
|
|
100
|
+
'Pass it to MarketSigner constructor as second argument or via MarketSignerOptions.');
|
|
101
|
+
}
|
|
102
|
+
const digest = computeDebitDigest({
|
|
103
|
+
channelId: params.channelId,
|
|
104
|
+
hub: params.hub,
|
|
105
|
+
token: params.token,
|
|
106
|
+
amount: params.amount,
|
|
107
|
+
receiptId: params.receiptId,
|
|
108
|
+
nonce: params.nonce,
|
|
109
|
+
deadline: BigInt(params.deadline),
|
|
110
|
+
chainId: params.chainId ?? 8453,
|
|
111
|
+
verifyingContract: (params.verifyingContract ??
|
|
112
|
+
'0x0000000000000000000000000000000000000000'),
|
|
113
|
+
});
|
|
114
|
+
const digestBytes = hexToBytes(digest);
|
|
115
|
+
const privBytes = hexToBytes(ethKey);
|
|
116
|
+
const recovered = secp256k1.sign(digestBytes, privBytes, {
|
|
117
|
+
lowS: true,
|
|
118
|
+
format: 'recovered',
|
|
119
|
+
});
|
|
120
|
+
const r = Buffer.from(recovered.slice(0, 32)).toString('hex');
|
|
121
|
+
const s = Buffer.from(recovered.slice(32, 64)).toString('hex');
|
|
122
|
+
const v = recovered[64] + 27;
|
|
123
|
+
return `eip712:0x${r}${s}${v.toString(16).padStart(2, '0')}`;
|
|
124
|
+
}
|
|
125
|
+
signEip712TypedData(_data) {
|
|
126
|
+
throw new Error('signEip712TypedData removed — use signDebitAuthorization() for AIMarketEscrow');
|
|
127
|
+
}
|
|
128
|
+
signedHeaders(args) {
|
|
129
|
+
const canonical = `channel:${args.channelId}|capability:${args.capabilityId}|affiliate:${args.affiliate}`;
|
|
130
|
+
return {
|
|
131
|
+
'X-Payment-Channel': args.channelId,
|
|
132
|
+
'X-AIMarket-Affiliate': args.affiliate,
|
|
133
|
+
'X-Market-Signature': this.signCanonical(canonical),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
verifyHubSignature(hubPublicKey, message, signature) {
|
|
137
|
+
if (signature.startsWith('eip712:'))
|
|
138
|
+
return false;
|
|
139
|
+
const canonical = signature.startsWith('ed25519:') ? message : message;
|
|
140
|
+
const sig = signature.startsWith('ed25519:') ? signature : `ed25519:${signature}`;
|
|
141
|
+
return this.verify(hubPublicKey, sig, canonical);
|
|
142
|
+
}
|
|
143
|
+
static generateEd25519Keypair() {
|
|
144
|
+
const seed = nacl.randomBytes(32);
|
|
145
|
+
const pair = nacl.sign.keyPair.fromSeed(seed);
|
|
146
|
+
return {
|
|
147
|
+
seedHex: Buffer.from(seed).toString('hex'),
|
|
148
|
+
publicKeyBase64: naclUtil.encodeBase64(pair.publicKey),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** Generate a secp256k1 Ethereum wallet for channel deposits / EIP-712. */
|
|
152
|
+
static generateEthereumWallet() {
|
|
153
|
+
const privateKey = generatePrivateKey();
|
|
154
|
+
const account = privateKeyToAccount(privateKey);
|
|
155
|
+
return { privateKeyHex: privateKey, address: account.address };
|
|
156
|
+
}
|
|
157
|
+
/** @deprecated Prefer [generateEd25519Keypair] or [generateEthereumWallet]. */
|
|
158
|
+
static generateWallet() {
|
|
159
|
+
const eth = MarketSigner.generateEthereumWallet();
|
|
160
|
+
return { privateKey: eth.privateKeyHex.slice(2), address: eth.address };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Re-export digest helper at package boundary.
|
|
164
|
+
export { computeDebitDigest } from './eip712';
|
package/dist/tee.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type TeeAttestation, type TeeReceipt } from './models';
|
|
2
|
+
import { MarketSigner } from './signer';
|
|
3
|
+
/** Recognized TEE platform identifiers. */
|
|
4
|
+
export declare const TeePlatform: {
|
|
5
|
+
readonly awsNitro: "aws_nitro";
|
|
6
|
+
readonly intelTdx: "intel_tdx";
|
|
7
|
+
readonly amdSev: "amd_sev";
|
|
8
|
+
readonly azureCc: "azure_cc";
|
|
9
|
+
readonly all: Set<string>;
|
|
10
|
+
readonly displayNames: {
|
|
11
|
+
readonly aws_nitro: "AWS Nitro Enclaves";
|
|
12
|
+
readonly intel_tdx: "Intel TDX";
|
|
13
|
+
readonly amd_sev: "AMD SEV-SNP";
|
|
14
|
+
readonly azure_cc: "Azure Confidential Computing";
|
|
15
|
+
};
|
|
16
|
+
readonly isSupported: (platform: string) => boolean;
|
|
17
|
+
};
|
|
18
|
+
/** Result of a TEE attestation verification with detailed failure reasons. */
|
|
19
|
+
export interface TeeVerificationResult {
|
|
20
|
+
isValid: boolean;
|
|
21
|
+
failures: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function teeVerificationPass(): TeeVerificationResult;
|
|
24
|
+
export declare function teeVerificationFail(failures: string[]): TeeVerificationResult;
|
|
25
|
+
/** Caches trusted code hashes fetched from the hub, with TTL expiry. */
|
|
26
|
+
export declare class TrustedHashCache {
|
|
27
|
+
private readonly ttlMs;
|
|
28
|
+
private readonly entries;
|
|
29
|
+
constructor(ttlMs?: number);
|
|
30
|
+
get(key: string): string | undefined;
|
|
31
|
+
set(key: string, hash: string): void;
|
|
32
|
+
clear(): void;
|
|
33
|
+
get size(): number;
|
|
34
|
+
}
|
|
35
|
+
/** Verifies TEE attestations and receipts client-side. */
|
|
36
|
+
export declare class TeeVerifier {
|
|
37
|
+
private readonly signer;
|
|
38
|
+
private readonly trustedCodeHashes;
|
|
39
|
+
private readonly hashCache;
|
|
40
|
+
private readonly enclavePublicKeys;
|
|
41
|
+
lastFetch: Date | null;
|
|
42
|
+
constructor(opts: {
|
|
43
|
+
signer: MarketSigner;
|
|
44
|
+
trustedCodeHashes?: Record<string, string>;
|
|
45
|
+
hashCache?: TrustedHashCache;
|
|
46
|
+
/** Override hub well-known enclave keys (hex or base64 public keys). */
|
|
47
|
+
enclavePublicKeys?: Record<string, string>;
|
|
48
|
+
});
|
|
49
|
+
trustCodeHash(capabilityId: string, codeHash: string): void;
|
|
50
|
+
fetchTrustedHashes(hubUrl: string, fetchFn?: typeof fetch): Promise<number>;
|
|
51
|
+
verifyAttestationDetailed(attestation: TeeAttestation, capabilityId: string): TeeVerificationResult;
|
|
52
|
+
verifyAttestation(attestation: TeeAttestation, capabilityId: string): boolean;
|
|
53
|
+
verifyReceipt(receipt: TeeReceipt, expectedInput: string, receivedOutput: string): boolean;
|
|
54
|
+
}
|
package/dist/tee.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { attestationCanonical } from './models';
|
|
3
|
+
/** Recognized TEE platform identifiers. */
|
|
4
|
+
export const TeePlatform = {
|
|
5
|
+
awsNitro: 'aws_nitro',
|
|
6
|
+
intelTdx: 'intel_tdx',
|
|
7
|
+
amdSev: 'amd_sev',
|
|
8
|
+
azureCc: 'azure_cc',
|
|
9
|
+
all: new Set(['aws_nitro', 'intel_tdx', 'amd_sev', 'azure_cc']),
|
|
10
|
+
displayNames: {
|
|
11
|
+
aws_nitro: 'AWS Nitro Enclaves',
|
|
12
|
+
intel_tdx: 'Intel TDX',
|
|
13
|
+
amd_sev: 'AMD SEV-SNP',
|
|
14
|
+
azure_cc: 'Azure Confidential Computing',
|
|
15
|
+
},
|
|
16
|
+
isSupported(platform) {
|
|
17
|
+
return TeePlatform.all.has(platform);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export function teeVerificationPass() {
|
|
21
|
+
return { isValid: true, failures: [] };
|
|
22
|
+
}
|
|
23
|
+
export function teeVerificationFail(failures) {
|
|
24
|
+
return { isValid: false, failures };
|
|
25
|
+
}
|
|
26
|
+
/** Caches trusted code hashes fetched from the hub, with TTL expiry. */
|
|
27
|
+
export class TrustedHashCache {
|
|
28
|
+
ttlMs;
|
|
29
|
+
entries = new Map();
|
|
30
|
+
constructor(ttlMs = 5 * 60 * 1000) {
|
|
31
|
+
this.ttlMs = ttlMs;
|
|
32
|
+
}
|
|
33
|
+
get(key) {
|
|
34
|
+
const entry = this.entries.get(key);
|
|
35
|
+
if (!entry)
|
|
36
|
+
return undefined;
|
|
37
|
+
if (Date.now() > entry.expiresAt) {
|
|
38
|
+
this.entries.delete(key);
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return entry.hash;
|
|
42
|
+
}
|
|
43
|
+
set(key, hash) {
|
|
44
|
+
this.entries.set(key, { hash, expiresAt: Date.now() + this.ttlMs });
|
|
45
|
+
}
|
|
46
|
+
clear() {
|
|
47
|
+
this.entries.clear();
|
|
48
|
+
}
|
|
49
|
+
get size() {
|
|
50
|
+
return this.entries.size;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function sha256Hex(input) {
|
|
54
|
+
return createHash('sha256').update(input, 'utf8').digest('hex');
|
|
55
|
+
}
|
|
56
|
+
function attestationIsExpired(attestation) {
|
|
57
|
+
const ts = Date.parse(attestation.timestamp);
|
|
58
|
+
if (Number.isNaN(ts))
|
|
59
|
+
return true;
|
|
60
|
+
const ageS = (Date.now() - ts) / 1000;
|
|
61
|
+
return ageS > attestation.ttl_s;
|
|
62
|
+
}
|
|
63
|
+
/** Verifies TEE attestations and receipts client-side. */
|
|
64
|
+
export class TeeVerifier {
|
|
65
|
+
signer;
|
|
66
|
+
trustedCodeHashes;
|
|
67
|
+
hashCache;
|
|
68
|
+
enclavePublicKeys;
|
|
69
|
+
lastFetch = null;
|
|
70
|
+
constructor(opts) {
|
|
71
|
+
this.signer = opts.signer;
|
|
72
|
+
this.trustedCodeHashes = new Map(Object.entries(opts.trustedCodeHashes ?? {}));
|
|
73
|
+
this.hashCache = opts.hashCache ?? new TrustedHashCache();
|
|
74
|
+
this.enclavePublicKeys = { ...DEFAULT_ENCLAVE_PUBLIC_KEYS, ...opts.enclavePublicKeys };
|
|
75
|
+
}
|
|
76
|
+
trustCodeHash(capabilityId, codeHash) {
|
|
77
|
+
this.trustedCodeHashes.set(capabilityId, codeHash);
|
|
78
|
+
this.hashCache.set(capabilityId, codeHash);
|
|
79
|
+
}
|
|
80
|
+
async fetchTrustedHashes(hubUrl, fetchFn = fetch) {
|
|
81
|
+
try {
|
|
82
|
+
const resp = await fetchFn(`${hubUrl.replace(/\/$/, '')}/.well-known/trusted-code-hashes.json`);
|
|
83
|
+
if (!resp.ok)
|
|
84
|
+
return -1;
|
|
85
|
+
const data = (await resp.json());
|
|
86
|
+
const hashes = data.hashes ?? [];
|
|
87
|
+
for (const entry of hashes) {
|
|
88
|
+
if (entry.capability_id && entry.code_hash) {
|
|
89
|
+
this.hashCache.set(entry.capability_id, entry.code_hash);
|
|
90
|
+
this.trustedCodeHashes.set(entry.capability_id, entry.code_hash);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
this.lastFetch = new Date();
|
|
94
|
+
return hashes.length;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return -1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
verifyAttestationDetailed(attestation, capabilityId) {
|
|
101
|
+
const failures = [];
|
|
102
|
+
if (!TeePlatform.isSupported(attestation.platform)) {
|
|
103
|
+
failures.push(`Unsupported TEE platform: ${attestation.platform}`);
|
|
104
|
+
}
|
|
105
|
+
const ts = Date.parse(attestation.timestamp);
|
|
106
|
+
if (Number.isNaN(ts)) {
|
|
107
|
+
failures.push(`Invalid attestation timestamp: ${attestation.timestamp}`);
|
|
108
|
+
}
|
|
109
|
+
else if (attestationIsExpired(attestation)) {
|
|
110
|
+
const ageS = Math.floor((Date.now() - ts) / 1000);
|
|
111
|
+
failures.push(`Attestation expired (age: ${ageS}s, ttl: ${attestation.ttl_s}s)`);
|
|
112
|
+
}
|
|
113
|
+
if (Object.keys(attestation.pcr_values).length === 0) {
|
|
114
|
+
failures.push('PCR values are empty — attestation lacks hardware proof');
|
|
115
|
+
}
|
|
116
|
+
const expectedHash = this.hashCache.get(capabilityId) ?? this.trustedCodeHashes.get(capabilityId);
|
|
117
|
+
if (expectedHash != null && attestation.code_hash !== expectedHash) {
|
|
118
|
+
failures.push(`Code hash mismatch: expected ${expectedHash}, got ${attestation.code_hash}`);
|
|
119
|
+
}
|
|
120
|
+
const enclaveKey = this.enclavePublicKeys[attestation.platform];
|
|
121
|
+
if (!enclaveKey) {
|
|
122
|
+
failures.push(`No known enclave public key for platform: ${attestation.platform}`);
|
|
123
|
+
}
|
|
124
|
+
else if (!this.signer.verify(enclaveKey, attestation.signature, attestationCanonical(attestation))) {
|
|
125
|
+
failures.push('Enclave signature verification failed');
|
|
126
|
+
}
|
|
127
|
+
return failures.length === 0 ? teeVerificationPass() : teeVerificationFail(failures);
|
|
128
|
+
}
|
|
129
|
+
verifyAttestation(attestation, capabilityId) {
|
|
130
|
+
return this.verifyAttestationDetailed(attestation, capabilityId).isValid;
|
|
131
|
+
}
|
|
132
|
+
verifyReceipt(receipt, expectedInput, receivedOutput) {
|
|
133
|
+
const inputHash = sha256Hex(expectedInput);
|
|
134
|
+
const outputHash = sha256Hex(receivedOutput);
|
|
135
|
+
if (receipt.input_hash !== inputHash)
|
|
136
|
+
return false;
|
|
137
|
+
if (receipt.output_hash !== outputHash)
|
|
138
|
+
return false;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Simulated defaults — production hubs publish keys at `/.well-known/enclave-keys.json`. */
|
|
143
|
+
const DEFAULT_ENCLAVE_PUBLIC_KEYS = {
|
|
144
|
+
aws_nitro: 'nitro_enclave_pubkey_hex',
|
|
145
|
+
intel_tdx: 'tdx_enclave_pubkey_hex',
|
|
146
|
+
amd_sev: 'sev_enclave_pubkey_hex',
|
|
147
|
+
azure_cc: 'azure_cc_pubkey_hex',
|
|
148
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aimarket/agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AI Market Protocol v2 consumer SDK for TypeScript — Electron, Node.js, web apps",
|
|
5
|
+
"repository": "https://github.com/alexar76/aimarket-sdks",
|
|
6
|
+
"homepage": "https://github.com/alexar76/aimarket-sdks/tree/main/typescript",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"lint": "eslint src/",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@noble/curves": "^2.2.0",
|
|
24
|
+
"@noble/hashes": "^2.2.0",
|
|
25
|
+
"tweetnacl": "^1.0.3",
|
|
26
|
+
"tweetnacl-util": "^0.15.1",
|
|
27
|
+
"viem": "^2.52.2"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"eslint": "^8.57.0",
|
|
32
|
+
"typescript": "^5.4.0",
|
|
33
|
+
"vitest": "^1.6.0"
|
|
34
|
+
}
|
|
35
|
+
}
|