@downcity/services 0.1.6

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.
Files changed (78) hide show
  1. package/README.md +47 -0
  2. package/bin/accounts/db.d.ts +19 -0
  3. package/bin/accounts/db.d.ts.map +1 -0
  4. package/bin/accounts/db.js +68 -0
  5. package/bin/accounts/db.js.map +1 -0
  6. package/bin/accounts/index.d.ts +348 -0
  7. package/bin/accounts/index.d.ts.map +1 -0
  8. package/bin/accounts/index.js +681 -0
  9. package/bin/accounts/index.js.map +1 -0
  10. package/bin/accounts/oauth.d.ts +129 -0
  11. package/bin/accounts/oauth.d.ts.map +1 -0
  12. package/bin/accounts/oauth.js +220 -0
  13. package/bin/accounts/oauth.js.map +1 -0
  14. package/bin/accounts/schema.d.ts +319 -0
  15. package/bin/accounts/schema.d.ts.map +1 -0
  16. package/bin/accounts/schema.js +72 -0
  17. package/bin/accounts/schema.js.map +1 -0
  18. package/bin/balance/index.d.ts +7 -0
  19. package/bin/balance/index.d.ts.map +1 -0
  20. package/bin/balance/index.js +6 -0
  21. package/bin/balance/index.js.map +1 -0
  22. package/bin/balance/raw.d.ts +20 -0
  23. package/bin/balance/raw.d.ts.map +1 -0
  24. package/bin/balance/raw.js +75 -0
  25. package/bin/balance/raw.js.map +1 -0
  26. package/bin/balance/routes.d.ts +14 -0
  27. package/bin/balance/routes.d.ts.map +1 -0
  28. package/bin/balance/routes.js +166 -0
  29. package/bin/balance/routes.js.map +1 -0
  30. package/bin/balance/schema.d.ts +764 -0
  31. package/bin/balance/schema.d.ts.map +1 -0
  32. package/bin/balance/schema.js +185 -0
  33. package/bin/balance/schema.js.map +1 -0
  34. package/bin/balance/service.d.ts +880 -0
  35. package/bin/balance/service.d.ts.map +1 -0
  36. package/bin/balance/service.js +557 -0
  37. package/bin/balance/service.js.map +1 -0
  38. package/bin/balance/types.d.ts +326 -0
  39. package/bin/balance/types.d.ts.map +1 -0
  40. package/bin/balance/types.js +10 -0
  41. package/bin/balance/types.js.map +1 -0
  42. package/bin/balance/utils.d.ts +91 -0
  43. package/bin/balance/utils.d.ts.map +1 -0
  44. package/bin/balance/utils.js +231 -0
  45. package/bin/balance/utils.js.map +1 -0
  46. package/bin/index.d.ts +22 -0
  47. package/bin/index.d.ts.map +1 -0
  48. package/bin/index.js +16 -0
  49. package/bin/index.js.map +1 -0
  50. package/bin/payment/index.d.ts +19 -0
  51. package/bin/payment/index.d.ts.map +1 -0
  52. package/bin/payment/index.js +63 -0
  53. package/bin/payment/index.js.map +1 -0
  54. package/bin/payment/types.d.ts +107 -0
  55. package/bin/payment/types.d.ts.map +1 -0
  56. package/bin/payment/types.js +10 -0
  57. package/bin/payment/types.js.map +1 -0
  58. package/bin/payment-stripe/index.d.ts +17 -0
  59. package/bin/payment-stripe/index.d.ts.map +1 -0
  60. package/bin/payment-stripe/index.js +619 -0
  61. package/bin/payment-stripe/index.js.map +1 -0
  62. package/bin/payment-stripe/schema.d.ts +378 -0
  63. package/bin/payment-stripe/schema.d.ts.map +1 -0
  64. package/bin/payment-stripe/schema.js +47 -0
  65. package/bin/payment-stripe/schema.js.map +1 -0
  66. package/bin/payment-stripe/stripe.d.ts +38 -0
  67. package/bin/payment-stripe/stripe.d.ts.map +1 -0
  68. package/bin/payment-stripe/stripe.js +129 -0
  69. package/bin/payment-stripe/stripe.js.map +1 -0
  70. package/bin/payment-stripe/types.d.ts +331 -0
  71. package/bin/payment-stripe/types.d.ts.map +1 -0
  72. package/bin/payment-stripe/types.js +10 -0
  73. package/bin/payment-stripe/types.js.map +1 -0
  74. package/bin/usage/index.d.ts +177 -0
  75. package/bin/usage/index.d.ts.map +1 -0
  76. package/bin/usage/index.js +120 -0
  77. package/bin/usage/index.js.map +1 -0
  78. package/package.json +60 -0
@@ -0,0 +1,681 @@
1
+ /**
2
+ * Downcity 官方 Accounts 服务。
3
+ *
4
+ * 设计边界:
5
+ * - better-auth 作为认证事实源,统一落到 `auth_users/auth_accounts/auth_sessions/auth_verifications`
6
+ * - 服务自己只维护 `auth_profiles`
7
+ * - OAuth 为兼容当前 CLI 轮询交互,保留自定义 callback 外壳
8
+ */
9
+ import { InstallableService } from "@downcity/infra";
10
+ import { betterAuth } from "better-auth";
11
+ import { getMigrations } from "better-auth/db/migration";
12
+ import { readPreparedAll, readPreparedFirst, runPrepared } from "./db.js";
13
+ import { ACCOUNTS_OAUTH_STATE_TABLE, USER_PROFILE_TABLE, accountsOAuthStates, userProfiles, } from "./schema.js";
14
+ import { OAUTH_PROVIDER_IDS, buildOAuthAuthorizeURL, buildSocialProviders, readOAuthProviderConfig, readOAuthProviderId, resolveOAuthProfile, } from "./oauth.js";
15
+ const AUTH_USER_TABLE = "auth_users";
16
+ const AUTH_ACCOUNT_TABLE = "auth_accounts";
17
+ const AUTH_SESSION_TABLE = "auth_sessions";
18
+ const AUTH_VERIFICATION_TABLE = "auth_verifications";
19
+ const OAUTH_STATE_TTL_MS = 5 * 60 * 1000;
20
+ const AUTH_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
21
+ export class AccountsService extends InstallableService {
22
+ options;
23
+ id = "accounts";
24
+ name = "Accounts";
25
+ version = "0.4.0";
26
+ schema = {
27
+ profile: userProfiles,
28
+ oauth_states: accountsOAuthStates,
29
+ };
30
+ auth;
31
+ constructor(options) {
32
+ super([
33
+ { key: "BETTER_AUTH_SECRET", description: "better-auth signing secret", required: true },
34
+ { key: "GITHUB_CLIENT_ID", description: "GitHub OAuth App Client ID", required: false },
35
+ { key: "GITHUB_CLIENT_SECRET", description: "GitHub OAuth App Client Secret", required: false },
36
+ { key: "GOOGLE_CLIENT_ID", description: "Google OAuth Client ID", required: false },
37
+ { key: "GOOGLE_CLIENT_SECRET", description: "Google OAuth Client Secret", required: false },
38
+ { key: "WECHAT_CLIENT_ID", description: "WeChat Website App AppID", required: false },
39
+ { key: "WECHAT_CLIENT_SECRET", description: "WeChat Website App AppSecret", required: false },
40
+ ]);
41
+ this.options = options;
42
+ this.instruction = ({ actions }) => [
43
+ "提供 Downcity 的账号、邮箱验证、GitHub/Google/WeChat OAuth 登录能力。",
44
+ "注册或登录时传入 product_id 后,接口会返回绑定该 product 的 InfraRuntime user_token。",
45
+ "OAuth 回调地址固定为 /v1/accounts/oauth/callback,服务会根据 InfraRuntime 公网地址生成完整回调 URL。",
46
+ `当前暴露 ${actions.length} 个动作,常用流程是 register/login -> verify-email 或 oauth/start -> me。`,
47
+ ].join("\n");
48
+ }
49
+ async _onInit() {
50
+ const sendEmail = this.options.sendEmail ?? defaultSendEmail;
51
+ this.auth = betterAuth({
52
+ secret: this._env?.get("BETTER_AUTH_SECRET"),
53
+ database: this._raw,
54
+ baseURL: this._baseURL,
55
+ user: {
56
+ modelName: AUTH_USER_TABLE,
57
+ },
58
+ account: {
59
+ modelName: AUTH_ACCOUNT_TABLE,
60
+ },
61
+ session: {
62
+ modelName: AUTH_SESSION_TABLE,
63
+ },
64
+ verification: {
65
+ modelName: AUTH_VERIFICATION_TABLE,
66
+ },
67
+ emailAndPassword: {
68
+ enabled: true,
69
+ autoSignIn: false,
70
+ },
71
+ emailVerification: {
72
+ sendVerificationEmail: async ({ user, url }) => {
73
+ await sendEmail({
74
+ to: user.email,
75
+ subject: "Verify your email for Downcity",
76
+ text: `Verification: ${url}`,
77
+ });
78
+ },
79
+ },
80
+ socialProviders: buildSocialProviders((key) => this._env?.get(key)),
81
+ });
82
+ await runBetterAuthMigrations(this.auth.options);
83
+ await super._onInit();
84
+ }
85
+ install(ctx) {
86
+ ctx.route({
87
+ method: "POST",
88
+ path: "/register",
89
+ auth: [],
90
+ handler: async (c) => {
91
+ const body = await c.json();
92
+ const email = String(body.email ?? "").trim().toLowerCase();
93
+ const password = String(body.password ?? "");
94
+ if (!email || !email.includes("@")) {
95
+ return c.jsonResponse({ error: "valid email required" }, 400);
96
+ }
97
+ if (password.length < 8) {
98
+ return c.jsonResponse({ error: "password must be at least 8 characters" }, 400);
99
+ }
100
+ try {
101
+ const result = await this.auth.api.signUpEmail({
102
+ body: { email, password, name: body.name ?? email.split("@")[0] ?? "" },
103
+ asResponse: true,
104
+ });
105
+ if (!result.ok) {
106
+ const err = await result.json().catch(() => ({}));
107
+ return c.jsonResponse({ error: err.message ?? "registration failed" }, result.status);
108
+ }
109
+ const data = await result.json();
110
+ if (data.user?.id) {
111
+ await this.upsertProfile({
112
+ user_id: data.user.id,
113
+ email: data.user.email,
114
+ display_name: String(data.user.name ?? data.user.email.split("@")[0] ?? ""),
115
+ avatar_url: String(data.user.image ?? ""),
116
+ });
117
+ }
118
+ return c.jsonResponse({
119
+ success: true,
120
+ message: "verification email sent",
121
+ verification_token: data.token,
122
+ user_id: data.user?.id,
123
+ });
124
+ }
125
+ catch (e) {
126
+ return c.jsonResponse({ error: readErrorMessage(e) }, 500);
127
+ }
128
+ },
129
+ });
130
+ ctx.route({
131
+ method: "POST",
132
+ path: "/verify-email",
133
+ auth: [],
134
+ handler: async (c) => {
135
+ const body = await c.json();
136
+ const token = String(body.token ?? "").trim();
137
+ if (!token)
138
+ return c.jsonResponse({ error: "verification token required" }, 400);
139
+ try {
140
+ const result = await this.auth.api.verifyEmail({
141
+ body: { token },
142
+ asResponse: true,
143
+ });
144
+ const data = await result.json();
145
+ const user_id = String(data.user?.id ?? "");
146
+ if (user_id) {
147
+ await this.upsertProfile({
148
+ user_id,
149
+ email: String(data.user?.email ?? ""),
150
+ display_name: String(data.user?.name ?? data.user?.email?.split("@")[0] ?? ""),
151
+ avatar_url: String(data.user?.image ?? ""),
152
+ });
153
+ }
154
+ const userToken = await ctx.createUserToken({
155
+ product_id: String(body.product_id ?? ""),
156
+ user_id,
157
+ ttl: this.options.token_ttl,
158
+ });
159
+ return c.jsonResponse({ user_token: userToken.user_token, user_id });
160
+ }
161
+ catch (e) {
162
+ return c.jsonResponse({ error: readErrorMessage(e) }, 500);
163
+ }
164
+ },
165
+ });
166
+ ctx.route({
167
+ method: "POST",
168
+ path: "/login",
169
+ auth: [],
170
+ handler: async (c) => {
171
+ const body = await c.json();
172
+ const email = String(body.email ?? "").trim().toLowerCase();
173
+ const password = String(body.password ?? "");
174
+ if (!email || !password) {
175
+ return c.jsonResponse({ error: "email and password required" }, 400);
176
+ }
177
+ try {
178
+ const result = await this.auth.api.signInEmail({
179
+ body: { email, password },
180
+ asResponse: true,
181
+ });
182
+ if (!result.ok) {
183
+ const err = await result.json().catch(() => ({}));
184
+ return c.jsonResponse({ error: err.message ?? "invalid email or password" }, 401);
185
+ }
186
+ const data = await result.json();
187
+ const user_id = String(data.user?.id ?? "");
188
+ if (user_id) {
189
+ await this.upsertProfile({
190
+ user_id,
191
+ email: String(data.user?.email ?? ""),
192
+ display_name: String(data.user?.name ?? data.user?.email?.split("@")[0] ?? ""),
193
+ avatar_url: String(data.user?.image ?? ""),
194
+ });
195
+ }
196
+ const userToken = await ctx.createUserToken({
197
+ product_id: String(body.product_id ?? ""),
198
+ user_id,
199
+ ttl: this.options.token_ttl,
200
+ });
201
+ return c.jsonResponse({
202
+ user_token: userToken.user_token,
203
+ user_id,
204
+ email: data.user?.email,
205
+ });
206
+ }
207
+ catch (e) {
208
+ return c.jsonResponse({ error: readErrorMessage(e) }, 500);
209
+ }
210
+ },
211
+ });
212
+ ctx.route({
213
+ method: "GET",
214
+ path: "/providers",
215
+ auth: [],
216
+ handler: async (c) => c.jsonResponse({ items: this.listProviders() }),
217
+ });
218
+ ctx.route({
219
+ method: "POST",
220
+ path: "/oauth/start",
221
+ auth: [],
222
+ handler: async (c) => {
223
+ const body = await c.json();
224
+ const provider = readOAuthProviderId(String(body.provider ?? "").trim());
225
+ if (!provider) {
226
+ return c.jsonResponse({ error: "provider must be github, google, or wechat" }, 400);
227
+ }
228
+ const config = this.getOAuthProviderConfig(provider);
229
+ if (!config) {
230
+ return c.jsonResponse({ error: "provider not configured" }, 400);
231
+ }
232
+ const product_id = String(body.product_id ?? "").trim() || "prod_downcity";
233
+ const state = randomToken(24);
234
+ await this.createOAuthState(product_id, provider, state);
235
+ const url = buildOAuthAuthorizeURL(config, this.getOAuthCallbackURL(), state);
236
+ return c.jsonResponse({ url, state, provider });
237
+ },
238
+ });
239
+ ctx.route({
240
+ method: "GET",
241
+ path: "/oauth/result",
242
+ auth: [],
243
+ handler: async (c) => {
244
+ const state = String(new URL(c.request.url).searchParams.get("state") ?? "").trim();
245
+ if (!state)
246
+ return c.jsonResponse({ error: "state required" }, 400);
247
+ const entry = await this.readOAuthState(state);
248
+ if (!entry)
249
+ return c.jsonResponse({ error: "state expired or invalid" }, 404);
250
+ if (!entry.user_token)
251
+ return c.jsonResponse({ status: "pending" });
252
+ return c.jsonResponse({ status: "done", user_token: entry.user_token });
253
+ },
254
+ });
255
+ ctx.route({
256
+ method: "GET",
257
+ path: "/me",
258
+ auth: ["user"],
259
+ handler: async (c) => {
260
+ const user_id = String(c.user?.user_id ?? "");
261
+ return c.jsonResponse({
262
+ user: c.user,
263
+ profile: user_id ? await this.readProfile(user_id) : null,
264
+ });
265
+ },
266
+ });
267
+ ctx.route({
268
+ method: "POST",
269
+ path: "/logout",
270
+ auth: ["user"],
271
+ handler: async (c) => c.jsonResponse({ success: true }),
272
+ });
273
+ ctx.route({
274
+ method: "GET",
275
+ path: "/users",
276
+ auth: ["admin"],
277
+ handler: async (c) => {
278
+ try {
279
+ return c.jsonResponse({ items: await this.listUsers() });
280
+ }
281
+ catch {
282
+ return c.jsonResponse({ items: [] });
283
+ }
284
+ },
285
+ });
286
+ ctx.route({
287
+ method: "GET",
288
+ path: "/sessions",
289
+ auth: ["admin"],
290
+ handler: async (c) => {
291
+ try {
292
+ return c.jsonResponse({ items: await this.listSessions() });
293
+ }
294
+ catch {
295
+ return c.jsonResponse({ items: [] });
296
+ }
297
+ },
298
+ });
299
+ }
300
+ /**
301
+ * 获取 better-auth 的 HTTP handler。
302
+ */
303
+ getAuthHandler() {
304
+ return this.auth.handler;
305
+ }
306
+ /**
307
+ * OAuth 回调处理。
308
+ */
309
+ async handleOAuthCallback(request) {
310
+ const url = new URL(request.url);
311
+ const error = url.searchParams.get("error");
312
+ const state = url.searchParams.get("state") ?? "";
313
+ const code = url.searchParams.get("code") ?? "";
314
+ if (error) {
315
+ const desc = url.searchParams.get("error_description") ?? error;
316
+ return new Response(OAUTH_ERROR_HTML.replace("{{ERROR}}", escapeHTML(desc)), {
317
+ status: 200,
318
+ headers: { "content-type": "text/html; charset=utf-8" },
319
+ });
320
+ }
321
+ try {
322
+ if (!state || !code)
323
+ throw new Error("Missing state or code");
324
+ const entry = await this.readOAuthState(state);
325
+ if (!entry)
326
+ throw new Error("OAuth state expired or invalid");
327
+ const provider = readOAuthProviderId(String(entry.provider));
328
+ if (!provider)
329
+ throw new Error("Invalid OAuth provider");
330
+ const profile = await resolveOAuthProfile(provider, this.getOAuthProviderConfig(provider), code, this.getOAuthCallbackURL());
331
+ const authUserId = await this.ensureOAuthAuthUser(profile, request);
332
+ const result = await this._authenticator.createToken({
333
+ product_id: entry.product_id,
334
+ user_id: authUserId,
335
+ ttl: this.options.token_ttl,
336
+ });
337
+ await this.resolveOAuthState(state, result.user_token);
338
+ }
339
+ catch (e) {
340
+ return new Response(OAUTH_ERROR_HTML.replace("{{ERROR}}", escapeHTML(readErrorMessage(e))), {
341
+ status: 200,
342
+ headers: { "content-type": "text/html; charset=utf-8" },
343
+ });
344
+ }
345
+ return new Response(OAUTH_SUCCESS_HTML, {
346
+ status: 200,
347
+ headers: { "content-type": "text/html; charset=utf-8" },
348
+ });
349
+ }
350
+ /**
351
+ * 列出可用登录方式。
352
+ */
353
+ listProviders() {
354
+ return [
355
+ {
356
+ id: "email",
357
+ type: "password",
358
+ enabled: true,
359
+ login_enabled: true,
360
+ register_enabled: true,
361
+ },
362
+ ...OAUTH_PROVIDER_IDS.map((provider) => {
363
+ const enabled = Boolean(this.getOAuthProviderConfig(provider));
364
+ return enabled
365
+ ? { id: provider, type: "oauth", enabled: true }
366
+ : { id: provider, type: "oauth", enabled: false, reason: "not_configured" };
367
+ }),
368
+ ];
369
+ }
370
+ /**
371
+ * 获取 provider 配置。
372
+ */
373
+ getOAuthProviderConfig(provider) {
374
+ if (provider === "github") {
375
+ return readOAuthProviderConfig(provider, this._env?.get("GITHUB_CLIENT_ID"), this._env?.get("GITHUB_CLIENT_SECRET"));
376
+ }
377
+ if (provider === "wechat") {
378
+ return readOAuthProviderConfig(provider, this._env?.get("WECHAT_CLIENT_ID"), this._env?.get("WECHAT_CLIENT_SECRET"));
379
+ }
380
+ return readOAuthProviderConfig(provider, this._env?.get("GOOGLE_CLIENT_ID"), this._env?.get("GOOGLE_CLIENT_SECRET"));
381
+ }
382
+ /**
383
+ * 当前请求环境下的 OAuth callback URL。
384
+ */
385
+ getOAuthCallbackURL() {
386
+ return `${this._baseURL}/v1/accounts/oauth/callback`;
387
+ }
388
+ /**
389
+ * 确保 OAuth 用户落库到 better-auth 认证表,并同步 `auth_profiles`。
390
+ */
391
+ async ensureOAuthAuthUser(profile, request) {
392
+ const now = new Date().toISOString();
393
+ const email = profile.email.trim().toLowerCase();
394
+ const existingAccount = await readPreparedFirst(this.rawPrepare(`SELECT id, userId FROM ${AUTH_ACCOUNT_TABLE} WHERE providerId = ? AND accountId = ? LIMIT 1`), [profile.provider, profile.provider_user_id]);
395
+ let user = existingAccount
396
+ ? await this.findAuthUserById(existingAccount.userId)
397
+ : email
398
+ ? await this.findAuthUserByEmail(email)
399
+ : null;
400
+ if (!user) {
401
+ user = {
402
+ id: prefixedId("usr"),
403
+ email,
404
+ emailVerified: profile.email_verified ? 1 : 0,
405
+ name: profile.display_name,
406
+ image: profile.avatar_url || null,
407
+ createdAt: now,
408
+ updatedAt: now,
409
+ };
410
+ await runPrepared(this.rawPrepare(`INSERT INTO ${AUTH_USER_TABLE} (id, email, emailVerified, name, image, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?)`), [user.id, user.email, normalizeBool(user.emailVerified), user.name, user.image, user.createdAt, user.updatedAt]);
411
+ }
412
+ else {
413
+ user = {
414
+ ...user,
415
+ email,
416
+ emailVerified: normalizeBool(user.emailVerified) || profile.email_verified ? 1 : 0,
417
+ name: profile.display_name || user.name,
418
+ image: profile.avatar_url || user.image,
419
+ updatedAt: now,
420
+ };
421
+ await runPrepared(this.rawPrepare(`UPDATE ${AUTH_USER_TABLE} SET email = ?, emailVerified = ?, name = ?, image = ?, updatedAt = ? WHERE id = ?`), [user.email, normalizeBool(user.emailVerified), user.name, user.image, user.updatedAt, user.id]);
422
+ }
423
+ if (!existingAccount) {
424
+ await runPrepared(this.rawPrepare(`INSERT INTO ${AUTH_ACCOUNT_TABLE} (id, accountId, providerId, userId, accessToken, refreshToken, idToken, accessTokenExpiresAt, refreshTokenExpiresAt, scope, password, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`), [
425
+ prefixedId("acc"),
426
+ profile.provider_user_id,
427
+ profile.provider,
428
+ user.id,
429
+ null,
430
+ null,
431
+ null,
432
+ null,
433
+ null,
434
+ null,
435
+ null,
436
+ now,
437
+ now,
438
+ ]);
439
+ }
440
+ else {
441
+ await runPrepared(this.rawPrepare(`UPDATE ${AUTH_ACCOUNT_TABLE} SET updatedAt = ? WHERE id = ?`), [now, existingAccount.id]);
442
+ }
443
+ await runPrepared(this.rawPrepare(`INSERT INTO ${AUTH_SESSION_TABLE} (id, expiresAt, token, createdAt, updatedAt, ipAddress, userAgent, userId) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`), [
444
+ prefixedId("sess"),
445
+ new Date(Date.now() + AUTH_SESSION_TTL_MS).toISOString(),
446
+ randomToken(32),
447
+ now,
448
+ now,
449
+ String(request.headers.get("x-forwarded-for") ?? ""),
450
+ String(request.headers.get("user-agent") ?? ""),
451
+ user.id,
452
+ ]);
453
+ await this.upsertProfile({
454
+ user_id: user.id,
455
+ email,
456
+ display_name: profile.display_name,
457
+ avatar_url: profile.avatar_url,
458
+ });
459
+ return user.id;
460
+ }
461
+ /**
462
+ * 创建 OAuth state。
463
+ */
464
+ async createOAuthState(product_id, provider, state) {
465
+ await runPrepared(this.rawPrepare(`INSERT INTO ${ACCOUNTS_OAUTH_STATE_TABLE} (state, product_id, provider, user_token, created_at) VALUES (?, ?, ?, ?, ?)`), [state, product_id, provider, "", Date.now()]);
466
+ }
467
+ /**
468
+ * 读取 OAuth state。
469
+ */
470
+ async readOAuthState(state) {
471
+ const row = await readPreparedFirst(this.rawPrepare(`SELECT state, product_id, provider, user_token, created_at FROM ${ACCOUNTS_OAUTH_STATE_TABLE} WHERE state = ?`), [state]);
472
+ if (!row)
473
+ return null;
474
+ if (Date.now() - Number(row.created_at) > OAUTH_STATE_TTL_MS) {
475
+ await runPrepared(this.rawPrepare(`DELETE FROM ${ACCOUNTS_OAUTH_STATE_TABLE} WHERE state = ?`), [state]);
476
+ return null;
477
+ }
478
+ return row;
479
+ }
480
+ /**
481
+ * 回填 OAuth state 的 InfraRuntime token。
482
+ */
483
+ async resolveOAuthState(state, user_token) {
484
+ await runPrepared(this.rawPrepare(`UPDATE ${ACCOUNTS_OAUTH_STATE_TABLE} SET user_token = ? WHERE state = ?`), [user_token, state]);
485
+ }
486
+ /**
487
+ * 按 ID 读取认证用户。
488
+ */
489
+ async findAuthUserById(user_id) {
490
+ return await readPreparedFirst(this.rawPrepare(`SELECT id, email, emailVerified, name, image, createdAt, updatedAt FROM ${AUTH_USER_TABLE} WHERE id = ? LIMIT 1`), [user_id]);
491
+ }
492
+ /**
493
+ * 按邮箱读取认证用户。
494
+ */
495
+ async findAuthUserByEmail(email) {
496
+ return await readPreparedFirst(this.rawPrepare(`SELECT id, email, emailVerified, name, image, createdAt, updatedAt FROM ${AUTH_USER_TABLE} WHERE lower(email) = ? LIMIT 1`), [email.toLowerCase()]);
497
+ }
498
+ /**
499
+ * 读取一条 user profile。
500
+ */
501
+ async readProfile(user_id) {
502
+ return await readPreparedFirst(this.rawPrepare(`SELECT user_id, email, display_name, avatar_url, bio, created_at, updated_at FROM ${USER_PROFILE_TABLE} WHERE user_id = ? LIMIT 1`), [user_id]);
503
+ }
504
+ /**
505
+ * upsert user profile。
506
+ */
507
+ async upsertProfile(input) {
508
+ const now = new Date().toISOString();
509
+ const existing = await this.readProfile(input.user_id);
510
+ if (existing) {
511
+ await runPrepared(this.rawPrepare(`UPDATE ${USER_PROFILE_TABLE} SET email = ?, display_name = ?, avatar_url = ?, updated_at = ? WHERE user_id = ?`), [
512
+ input.email || existing.email,
513
+ input.display_name || existing.display_name,
514
+ input.avatar_url || existing.avatar_url,
515
+ now,
516
+ input.user_id,
517
+ ]);
518
+ return;
519
+ }
520
+ await runPrepared(this.rawPrepare(`INSERT INTO ${USER_PROFILE_TABLE} (user_id, email, display_name, avatar_url, bio, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`), [input.user_id, input.email, input.display_name, input.avatar_url, "", now, now]);
521
+ }
522
+ /**
523
+ * 管理侧用户列表。
524
+ */
525
+ async listUsers() {
526
+ return await readPreparedAll(this.rawPrepare(`SELECT
527
+ u.id as user_id,
528
+ u.email as auth_email,
529
+ u.emailVerified as email_verified,
530
+ u.name as auth_name,
531
+ u.image as auth_image,
532
+ u.createdAt as auth_created_at,
533
+ u.updatedAt as auth_updated_at,
534
+ p.email as profile_email,
535
+ p.display_name,
536
+ p.avatar_url,
537
+ p.bio,
538
+ p.created_at as profile_created_at,
539
+ p.updated_at as profile_updated_at
540
+ FROM ${AUTH_USER_TABLE} u
541
+ LEFT JOIN ${USER_PROFILE_TABLE} p ON p.user_id = u.id
542
+ ORDER BY u.createdAt DESC`), []);
543
+ }
544
+ /**
545
+ * 管理侧 session 列表。
546
+ */
547
+ async listSessions() {
548
+ const rows = await readPreparedAll(this.rawPrepare(`SELECT id as session_id, userId as user_id, expiresAt as expires_at, createdAt as created_at FROM ${AUTH_SESSION_TABLE} ORDER BY expiresAt DESC`), []);
549
+ return rows.map((row) => ({
550
+ ...row,
551
+ status: new Date(String(row.expires_at ?? "")).getTime() > Date.now() ? "active" : "expired",
552
+ }));
553
+ }
554
+ /**
555
+ * 创建原始 statement。
556
+ */
557
+ rawPrepare(sql) {
558
+ return this._raw.prepare(sql);
559
+ }
560
+ }
561
+ /**
562
+ * 创建 Accounts 服务实例。
563
+ */
564
+ export function accountsService(options = {}) {
565
+ return new AccountsService(options);
566
+ }
567
+ /**
568
+ * 运行 better-auth migrations。
569
+ */
570
+ async function runBetterAuthMigrations(options) {
571
+ try {
572
+ const { runMigrations } = await getMigrations(options);
573
+ await runMigrations();
574
+ }
575
+ catch {
576
+ // better-auth 内部仍然会做兜底,这里保持静默容错。
577
+ }
578
+ }
579
+ /**
580
+ * 默认开发邮件发送器。
581
+ */
582
+ async function defaultSendEmail(params) {
583
+ console.log("[accounts] VERIFICATION EMAIL");
584
+ console.log(` To: ${params.to}`);
585
+ console.log(` Subject: ${params.subject}`);
586
+ console.log(` Body: ${params.text}`);
587
+ }
588
+ /**
589
+ * 生成带前缀的稳定 ID。
590
+ */
591
+ function prefixedId(prefix) {
592
+ return `${prefix}_${crypto.randomUUID().replaceAll("-", "")}`;
593
+ }
594
+ /**
595
+ * 生成 URL-safe token。
596
+ */
597
+ function randomToken(size) {
598
+ const buf = new Uint8Array(size);
599
+ crypto.getRandomValues(buf);
600
+ let binary = "";
601
+ for (let i = 0; i < buf.length; i++)
602
+ binary += String.fromCharCode(buf[i]);
603
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
604
+ }
605
+ /**
606
+ * 归一化布尔值到 SQLite 整数。
607
+ */
608
+ function normalizeBool(value) {
609
+ return value ? 1 : 0;
610
+ }
611
+ /**
612
+ * 读取错误消息。
613
+ */
614
+ function readErrorMessage(error) {
615
+ return error instanceof Error ? error.message : String(error);
616
+ }
617
+ /**
618
+ * 转义 HTML。
619
+ */
620
+ function escapeHTML(value) {
621
+ return value
622
+ .replaceAll("&", "&amp;")
623
+ .replaceAll("<", "&lt;")
624
+ .replaceAll(">", "&gt;")
625
+ .replaceAll("\"", "&quot;")
626
+ .replaceAll("'", "&#39;");
627
+ }
628
+ // ===========================================================================
629
+ // OAuth 成功页面
630
+ // ===========================================================================
631
+ /**
632
+ * OAuth 登录失败页面。
633
+ */
634
+ const OAUTH_ERROR_HTML = `<!DOCTYPE html>
635
+ <html lang="en">
636
+ <head>
637
+ <meta charset="UTF-8">
638
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
639
+ <title>Login Failed — Downcity</title>
640
+ <style>
641
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
642
+ display: flex; justify-content: center; align-items: center;
643
+ min-height: 100vh; margin: 0; background: #0a0a0a; color: #e0e0e0; }
644
+ .box { text-align: center; padding: 2rem; }
645
+ h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; color: #ff7c7c; }
646
+ p { color: #888; font-size: 0.9rem; }
647
+ </style>
648
+ </head>
649
+ <body>
650
+ <div class="box">
651
+ <h1>✗ Login Failed</h1>
652
+ <p>Error: {{ERROR}}</p>
653
+ </div>
654
+ </body>
655
+ </html>`;
656
+ /**
657
+ * OAuth 登录成功页面。
658
+ */
659
+ const OAUTH_SUCCESS_HTML = `<!DOCTYPE html>
660
+ <html lang="en">
661
+ <head>
662
+ <meta charset="UTF-8">
663
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
664
+ <title>Login Successful — Downcity</title>
665
+ <style>
666
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
667
+ display: flex; justify-content: center; align-items: center;
668
+ min-height: 100vh; margin: 0; background: #0a0a0a; color: #e0e0e0; }
669
+ .box { text-align: center; padding: 2rem; }
670
+ h1 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; color: #7cff7c; }
671
+ p { color: #888; font-size: 0.9rem; }
672
+ </style>
673
+ </head>
674
+ <body>
675
+ <div class="box">
676
+ <h1>✓ Login Successful</h1>
677
+ <p>You can close this window and return to the CLI.</p>
678
+ </div>
679
+ </body>
680
+ </html>`;
681
+ //# sourceMappingURL=index.js.map