repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
public Clawd ADK gateway launch mirror
stars
latest
clone command
git clone gitlawb://did:key:z6Mkq5mY...iFZ5/my-project-publ...git clone gitlawb://did:key:z6Mkq5mY.../my-project-publ...2fa351d6docs: add automaton and perps launch sources16d ago| #1 | // ═══════════════════════════════════════════════════════════════ |
| #2 | // AGENT SERVICE - Database operations and business logic |
| #3 | // ═══════════════════════════════════════════════════════════════ |
| #4 | |
| #5 | import type { Env, Agent, AgentPermissions } from '../index'; |
| #6 | |
| #7 | // ───────────────────────────────────────────────── |
| #8 | // CRYPTO HELPERS |
| #9 | // ───────────────────────────────────────────────── |
| #10 | |
| #11 | async function generateApiKey(): Promise<string> { |
| #12 | const array = new Uint8Array(32); |
| #13 | crypto.getRandomValues(array); |
| #14 | const hex = Array.from(array).map(b => b.toString(16).padStart(2, '0')).join(''); |
| #15 | return `agent_${hex}`; |
| #16 | } |
| #17 | |
| #18 | async function hashString(str: string): Promise<string> { |
| #19 | const encoder = new TextEncoder(); |
| #20 | const data = encoder.encode(str); |
| #21 | const hashBuffer = await crypto.subtle.digest('SHA-256', data); |
| #22 | const hashArray = Array.from(new Uint8Array(hashBuffer)); |
| #23 | return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); |
| #24 | } |
| #25 | |
| #26 | function generateId(prefix: string): string { |
| #27 | const array = new Uint8Array(12); |
| #28 | crypto.getRandomValues(array); |
| #29 | const hex = Array.from(array).map(b => b.toString(16).padStart(2, '0')).join(''); |
| #30 | return `${prefix}_${hex}`; |
| #31 | } |
| #32 | |
| #33 | // ───────────────────────────────────────────────── |
| #34 | // AGENT SERVICE CLASS |
| #35 | // ───────────────────────────────────────────────── |
| #36 | |
| #37 | export class AgentService { |
| #38 | private db: D1Database; |
| #39 | private sessions: KVNamespace; |
| #40 | private rateLimits: KVNamespace; |
| #41 | private sessionDurationMs = 24 * 60 * 60 * 1000; // 24 hours |
| #42 | |
| #43 | constructor(private env: Env) { |
| #44 | this.db = env.DB; |
| #45 | this.sessions = env.SESSIONS; |
| #46 | this.rateLimits = env.RATE_LIMITS; |
| #47 | } |
| #48 | |
| #49 | // ═══════════════════════════════════════════════════ |
| #50 | // REGISTRATION |
| #51 | // ═══════════════════════════════════════════════════ |
| #52 | |
| #53 | async register(params: { |
| #54 | name: string; |
| #55 | description?: string; |
| #56 | chain?: 'solana' | 'solana-devnet'; |
| #57 | metadata?: Record<string, unknown>; |
| #58 | }): Promise<{ agent: Agent; apiKey: string }> { |
| #59 | const id = generateId('agt'); |
| #60 | const apiKey = await generateApiKey(); |
| #61 | const apiKeyHash = await hashString(apiKey); |
| #62 | const apiKeyPrefix = apiKey.slice(0, 12) + '...'; |
| #63 | |
| #64 | const defaultPermissions: AgentPermissions = { |
| #65 | canCreateWallet: true, |
| #66 | canTransfer: true, |
| #67 | canSwap: true, |
| #68 | maxTransferAmount: 100, |
| #69 | maxDailyVolume: 1000, |
| #70 | }; |
| #71 | |
| #72 | await this.db.prepare(` |
| #73 | INSERT INTO agents (id, name, description, api_key_hash, api_key_prefix, chain, permissions, metadata) |
| #74 | VALUES (?, ?, ?, ?, ?, ?, ?, ?) |
| #75 | `).bind( |
| #76 | id, |
| #77 | params.name, |
| #78 | params.description || null, |
| #79 | apiKeyHash, |
| #80 | apiKeyPrefix, |
| #81 | params.chain || 'solana-devnet', |
| #82 | JSON.stringify(defaultPermissions), |
| #83 | params.metadata ? JSON.stringify(params.metadata) : null |
| #84 | ).run(); |
| #85 | |
| #86 | // Record API key history |
| #87 | await this.db.prepare(` |
| #88 | INSERT INTO api_key_history (agent_id, api_key_prefix) |
| #89 | VALUES (?, ?) |
| #90 | `).bind(id, apiKeyPrefix).run(); |
| #91 | |
| #92 | const agent = await this.getAgentById(id); |
| #93 | if (!agent) { |
| #94 | throw new Error('Failed to create agent'); |
| #95 | } |
| #96 | |
| #97 | return { agent, apiKey }; |
| #98 | } |
| #99 | |
| #100 | // ═══════════════════════════════════════════════════ |
| #101 | // AUTHENTICATION |
| #102 | // ═══════════════════════════════════════════════════ |
| #103 | |
| #104 | async login( |
| #105 | apiKey: string, |
| #106 | ipAddress: string | null, |
| #107 | userAgent: string | null |
| #108 | ): Promise<{ agent: Agent; sessionToken: string; expiresAt: string } | null> { |
| #109 | const apiKeyHash = await hashString(apiKey); |
| #110 | |
| #111 | const result = await this.db.prepare(` |
| #112 | SELECT * FROM agents WHERE api_key_hash = ? AND status = 'active' |
| #113 | `).bind(apiKeyHash).first<Agent>(); |
| #114 | |
| #115 | if (!result) { |
| #116 | return null; |
| #117 | } |
| #118 | |
| #119 | // Parse permissions JSON |
| #120 | const agent = this.parseAgentRow(result); |
| #121 | |
| #122 | // Create session |
| #123 | const sessionId = generateId('ses'); |
| #124 | const sessionToken = await generateApiKey(); // Reuse for random token |
| #125 | const tokenHash = await hashString(sessionToken); |
| #126 | const now = new Date(); |
| #127 | const expiresAt = new Date(now.getTime() + this.sessionDurationMs); |
| #128 | |
| #129 | // Store session in D1 |
| #130 | await this.db.prepare(` |
| #131 | INSERT INTO sessions (id, agent_id, token_hash, ip_address, user_agent, expires_at) |
| #132 | VALUES (?, ?, ?, ?, ?, ?) |
| #133 | `).bind( |
| #134 | sessionId, |
| #135 | agent.id, |
| #136 | tokenHash, |
| #137 | ipAddress, |
| #138 | userAgent, |
| #139 | expiresAt.toISOString() |
| #140 | ).run(); |
| #141 | |
| #142 | // Also store in KV for fast lookup |
| #143 | await this.sessions.put(tokenHash, JSON.stringify({ |
| #144 | agentId: agent.id, |
| #145 | expiresAt: expiresAt.toISOString(), |
| #146 | }), { expirationTtl: Math.floor(this.sessionDurationMs / 1000) }); |
| #147 | |
| #148 | // Update last active |
| #149 | await this.db.prepare(` |
| #150 | UPDATE agents SET last_active_at = datetime('now') WHERE id = ? |
| #151 | `).bind(agent.id).run(); |
| #152 | |
| #153 | return { |
| #154 | agent, |
| #155 | sessionToken, |
| #156 | expiresAt: expiresAt.toISOString(), |
| #157 | }; |
| #158 | } |
| #159 | |
| #160 | async validateApiKey(apiKey: string): Promise<Agent | null> { |
| #161 | const apiKeyHash = await hashString(apiKey); |
| #162 | |
| #163 | const result = await this.db.prepare(` |
| #164 | SELECT * FROM agents WHERE api_key_hash = ? AND status = 'active' |
| #165 | `).bind(apiKeyHash).first<Agent>(); |
| #166 | |
| #167 | if (!result) { |
| #168 | return null; |
| #169 | } |
| #170 | |
| #171 | // Update last active |
| #172 | await this.db.prepare(` |
| #173 | UPDATE agents SET last_active_at = datetime('now') WHERE id = ? |
| #174 | `).bind(result.id).run(); |
| #175 | |
| #176 | return this.parseAgentRow(result); |
| #177 | } |
| #178 | |
| #179 | async validateSession(sessionToken: string): Promise<Agent | null> { |
| #180 | const tokenHash = await hashString(sessionToken); |
| #181 | |
| #182 | // Check KV first (faster) |
| #183 | const kvSession = await this.sessions.get(tokenHash); |
| #184 | if (kvSession) { |
| #185 | const { agentId, expiresAt } = JSON.parse(kvSession); |
| #186 | |
| #187 | if (new Date() > new Date(expiresAt)) { |
| #188 | await this.sessions.delete(tokenHash); |
| #189 | return null; |
| #190 | } |
| #191 | |
| #192 | const agent = await this.getAgentById(agentId); |
| #193 | if (agent && agent.status === 'active') { |
| #194 | // Update last active |
| #195 | await this.db.prepare(` |
| #196 | UPDATE agents SET last_active_at = datetime('now') WHERE id = ? |
| #197 | `).bind(agentId).run(); |
| #198 | |
| #199 | // Update session last used |
| #200 | await this.db.prepare(` |
| #201 | UPDATE sessions SET last_used_at = datetime('now') WHERE token_hash = ? |
| #202 | `).bind(tokenHash).run(); |
| #203 | |
| #204 | return agent; |
| #205 | } |
| #206 | } |
| #207 | |
| #208 | // Fallback to D1 |
| #209 | const session = await this.db.prepare(` |
| #210 | SELECT * FROM sessions WHERE token_hash = ? AND expires_at > datetime('now') |
| #211 | `).bind(tokenHash).first(); |
| #212 | |
| #213 | if (!session) { |
| #214 | return null; |
| #215 | } |
| #216 | |
| #217 | const agent = await this.getAgentById(session.agent_id as string); |
| #218 | if (!agent || agent.status !== 'active') { |
| #219 | return null; |
| #220 | } |
| #221 | |
| #222 | // Update last active |
| #223 | await this.db.prepare(` |
| #224 | UPDATE agents SET last_active_at = datetime('now') WHERE id = ? |
| #225 | `).bind(agent.id).run(); |
| #226 | |
| #227 | return agent; |
| #228 | } |
| #229 | |
| #230 | async invalidateSession(sessionToken: string): Promise<void> { |
| #231 | const tokenHash = await hashString(sessionToken); |
| #232 | |
| #233 | await this.sessions.delete(tokenHash); |
| #234 | await this.db.prepare(` |
| #235 | DELETE FROM sessions WHERE token_hash = ? |
| #236 | `).bind(tokenHash).run(); |
| #237 | } |
| #238 | |
| #239 | // ═══════════════════════════════════════════════════ |
| #240 | // AGENT MANAGEMENT |
| #241 | // ═══════════════════════════════════════════════════ |
| #242 | |
| #243 | async getAgentById(id: string): Promise<Agent | null> { |
| #244 | const result = await this.db.prepare(` |
| #245 | SELECT * FROM agents WHERE id = ? |
| #246 | `).bind(id).first<Agent>(); |
| #247 | |
| #248 | return result ? this.parseAgentRow(result) : null; |
| #249 | } |
| #250 | |
| #251 | async updateWallet(agentId: string, walletAddress: string): Promise<void> { |
| #252 | await this.db.prepare(` |
| #253 | UPDATE agents SET wallet_address = ?, updated_at = datetime('now') WHERE id = ? |
| #254 | `).bind(walletAddress, agentId).run(); |
| #255 | } |
| #256 | |
| #257 | async regenerateApiKey(agentId: string): Promise<string> { |
| #258 | const newApiKey = await generateApiKey(); |
| #259 | const newApiKeyHash = await hashString(newApiKey); |
| #260 | const newApiKeyPrefix = newApiKey.slice(0, 12) + '...'; |
| #261 | |
| #262 | // Get old key prefix for history |
| #263 | const agent = await this.getAgentById(agentId); |
| #264 | if (!agent) { |
| #265 | throw new Error('Agent not found'); |
| #266 | } |
| #267 | |
| #268 | // Update old key history as revoked |
| #269 | await this.db.prepare(` |
| #270 | UPDATE api_key_history |
| #271 | SET revoked_at = datetime('now'), revoke_reason = 'regenerated' |
| #272 | WHERE agent_id = ? AND revoked_at IS NULL |
| #273 | `).bind(agentId).run(); |
| #274 | |
| #275 | // Update agent with new key |
| #276 | await this.db.prepare(` |
| #277 | UPDATE agents |
| #278 | SET api_key_hash = ?, api_key_prefix = ?, updated_at = datetime('now') |
| #279 | WHERE id = ? |
| #280 | `).bind(newApiKeyHash, newApiKeyPrefix, agentId).run(); |
| #281 | |
| #282 | // Add new key to history |
| #283 | await this.db.prepare(` |
| #284 | INSERT INTO api_key_history (agent_id, api_key_prefix) |
| #285 | VALUES (?, ?) |
| #286 | `).bind(agentId, newApiKeyPrefix).run(); |
| #287 | |
| #288 | // Invalidate all sessions for this agent |
| #289 | await this.db.prepare(` |
| #290 | DELETE FROM sessions WHERE agent_id = ? |
| #291 | `).bind(agentId).run(); |
| #292 | |
| #293 | return newApiKey; |
| #294 | } |
| #295 | |
| #296 | // ═══════════════════════════════════════════════════ |
| #297 | // RATE LIMITING |
| #298 | // ═══════════════════════════════════════════════════ |
| #299 | |
| #300 | async checkRateLimit(agentId: string): Promise<{ |
| #301 | allowed: boolean; |
| #302 | remaining: { minute: number; day: number }; |
| #303 | }> { |
| #304 | const agent = await this.getAgentById(agentId); |
| #305 | if (!agent) { |
| #306 | throw new Error('Agent not found'); |
| #307 | } |
| #308 | |
| #309 | const now = Date.now(); |
| #310 | const minuteKey = `rate:${agentId}:minute:${Math.floor(now / 60000)}`; |
| #311 | const dayKey = `rate:${agentId}:day:${Math.floor(now / 86400000)}`; |
| #312 | |
| #313 | const [minuteCount, dayCount] = await Promise.all([ |
| #314 | this.rateLimits.get(minuteKey).then(v => parseInt(v || '0', 10)), |
| #315 | this.rateLimits.get(dayKey).then(v => parseInt(v || '0', 10)), |
| #316 | ]); |
| #317 | |
| #318 | const allowed = |
| #319 | minuteCount < agent.requests_per_minute && |
| #320 | dayCount < agent.requests_per_day; |
| #321 | |
| #322 | return { |
| #323 | allowed, |
| #324 | remaining: { |
| #325 | minute: Math.max(0, agent.requests_per_minute - minuteCount), |
| #326 | day: Math.max(0, agent.requests_per_day - dayCount), |
| #327 | }, |
| #328 | }; |
| #329 | } |
| #330 | |
| #331 | async recordRequest(agentId: string): Promise<void> { |
| #332 | const now = Date.now(); |
| #333 | const minuteKey = `rate:${agentId}:minute:${Math.floor(now / 60000)}`; |
| #334 | const dayKey = `rate:${agentId}:day:${Math.floor(now / 86400000)}`; |
| #335 | |
| #336 | const [minuteCount, dayCount] = await Promise.all([ |
| #337 | this.rateLimits.get(minuteKey).then(v => parseInt(v || '0', 10)), |
| #338 | this.rateLimits.get(dayKey).then(v => parseInt(v || '0', 10)), |
| #339 | ]); |
| #340 | |
| #341 | await Promise.all([ |
| #342 | this.rateLimits.put(minuteKey, String(minuteCount + 1), { expirationTtl: 120 }), |
| #343 | this.rateLimits.put(dayKey, String(dayCount + 1), { expirationTtl: 86400 }), |
| #344 | ]); |
| #345 | } |
| #346 | |
| #347 | // ═══════════════════════════════════════════════════ |
| #348 | // ACTIVITY LOGGING |
| #349 | // ═══════════════════════════════════════════════════ |
| #350 | |
| #351 | async logActivity( |
| #352 | agentId: string, |
| #353 | action: string, |
| #354 | details: Record<string, unknown>, |
| #355 | ipAddress: string | null, |
| #356 | status: 'success' | 'failed' | 'pending' = 'success', |
| #357 | errorMessage?: string |
| #358 | ): Promise<void> { |
| #359 | await this.db.prepare(` |
| #360 | INSERT INTO agent_activity (agent_id, action, details, ip_address, status, error_message) |
| #361 | VALUES (?, ?, ?, ?, ?, ?) |
| #362 | `).bind( |
| #363 | agentId, |
| #364 | action, |
| #365 | JSON.stringify(details), |
| #366 | ipAddress, |
| #367 | status, |
| #368 | errorMessage || null |
| #369 | ).run(); |
| #370 | } |
| #371 | |
| #372 | // ───────────────────────────────────────────────── |
| #373 | // HELPERS |
| #374 | // ───────────────────────────────────────────────── |
| #375 | |
| #376 | private parseAgentRow(row: Agent): Agent { |
| #377 | return { |
| #378 | ...row, |
| #379 | permissions: typeof row.permissions === 'string' |
| #380 | ? JSON.parse(row.permissions) |
| #381 | : row.permissions, |
| #382 | metadata: row.metadata && typeof row.metadata === 'string' |
| #383 | ? JSON.parse(row.metadata) |
| #384 | : row.metadata, |
| #385 | }; |
| #386 | } |
| #387 | } |
| #388 |