runtime.js
3.28 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* voko-server 的 composition root:读取 env、创建连接、装配服务,并统一管理资源生命周期。
*/
import { fileURLToPath } from 'node:url'
import { config as loadDotenv } from 'dotenv'
import { createAuth, createUserRepo } from '@drama/auth'
import { createBillingRepo, createLedger } from '@drama/billing'
import { createDb } from '@drama/db'
import { createLogger } from '@drama/observability'
import { createAccessRequests, createAgentAccess } from '@voko/access'
import { createAgents, createGuilds } from '@voko/exchange'
import { readEnv } from '../env.js'
import { createEmailPorts } from './email.js'
import { createGoogleOauthPort } from './google-oauth.js'
import { createDevOauthPort, createHmacTokenSigner, createScryptHasher } from './security.js'
/**
* @param {{ service: string }} opts
*/
export async function createRuntime({ service }) {
// 1. 加载 .env 并解析配置。
loadDotenv({ path: fileURLToPath(new URL('../../.env', import.meta.url)) })
const env = readEnv(process.env)
const logger = createLogger({
service,
level: env.logLevel,
format: env.nodeEnv === 'production' ? 'json' : 'pretty',
})
// 2. 连接与基础端口。
const db = createDb({ url: env.databaseUrl })
const { authEmail, accessNotifier } = createEmailPorts({ env, db, logger })
const tokenSigner = createHmacTokenSigner({ secret: env.authTokenSecret })
const hasher = createScryptHasher()
const oauth = env.oauth.googleClientId === null
? createDevOauthPort()
: createGoogleOauthPort({ clientId: env.oauth.googleClientId, clientSecret: env.oauth.googleClientSecret })
// 3. 领域服务。
const userRepo = createUserRepo(db)
const auth = createAuth({ userRepo, email: authEmail, oauth, tokenSigner, hasher })
const access = createAccessRequests({ db, notifier: accessNotifier })
const agents = createAgents()
const guilds = createGuilds({ db })
const agentAccess = createAgentAccess({
db,
agents,
public_url: env.publicUrl,
// 启动积分 100:在认领事务里记账,幂等键由 agent_id 唯一决定。
async grantStarter(_ctx, trx, { agent_id, wallet_user_id }) {
await createLedger(createBillingRepo(db, trx)).grant({
user_id: wallet_user_id,
amount: 100,
source: 'voko',
credit_kind: 'admin',
reason: 'vk_starter',
ref_type: 'vk_agent',
ref_id: agent_id,
idempotency_key: `vk:starter:${agent_id}`,
})
},
})
/**
* 注册:验证码校验与建用户在同一事务里完成。
* @param {{ email: string, password: string, verification_code: string, locale: string }} input
*/
async function register(input) {
return db.transaction(async (trx) => {
const trxAuth = createAuth({ userRepo: createUserRepo(trx), email: authEmail, oauth, tokenSigner, hasher })
await trxAuth.verifyCode({ email: input.email, code: input.verification_code, purpose: 'register' })
return trxAuth.register({ email: input.email, password: input.password, locale: input.locale, email_verified_at: new Date() })
})
}
return {
env,
logger,
services: { db, auth, userRepo, access, agents, guilds, agentAccess, register, oauthEnabled: env.oauth.googleClientId !== null },
async close() {
await db.destroy()
},
}
}