@bbki.ng/backend 0.0.1 → 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/CHANGELOG.md CHANGED
@@ -1,3 +1,5 @@
1
1
  # backend
2
2
 
3
+ ## 0.1.0
4
+
3
5
  ## 0.0.2
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@bbki.ng/backend",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "dependencies": {
6
+ "@simplewebauthn/server": "13.2.2",
6
7
  "hono": "^4.10.7"
7
8
  },
8
9
  "devDependencies": {
9
10
  "@cloudflare/workers-types": "4.20251128.0",
10
- "wrangler": "^4.4.0"
11
+ "@types/node": "25.0.3",
12
+ "wrangler": "^4.58.0"
11
13
  },
12
14
  "scripts": {
13
15
  "dev": "wrangler dev",
@@ -0,0 +1,18 @@
1
+ import { Hono } from "hono";
2
+ import type { D1Database } from "@cloudflare/workers-types";
3
+ import { cors } from "hono/cors";
4
+
5
+ type Bindings = {
6
+ DB: D1Database;
7
+ };
8
+
9
+ const app = new Hono<{ Bindings: Bindings }>();
10
+
11
+ app.use(
12
+ "*",
13
+ cors({
14
+ origin: ["https://bbki.ng"],
15
+ }),
16
+ );
17
+
18
+ export default app;
@@ -0,0 +1,98 @@
1
+ import { Context } from "hono";
2
+ import { Passkey, User } from "../../types";
3
+
4
+ export const createDatabaseQuery = (c: Context) => {
5
+ const env = c.env;
6
+ return {
7
+ // 用户相关
8
+ async getUserByUsername(username: string): Promise<User | null> {
9
+ // TODO: 替换为 D1 SQL 查询
10
+ const stmt = env.DB.prepare(
11
+ "SELECT id, username, display_name FROM users WHERE username = ?",
12
+ ).bind(username);
13
+ const result = await stmt.first();
14
+ return result
15
+ ? {
16
+ id: result.id as string,
17
+ username: result.username as string,
18
+ displayName: result.display_name as string,
19
+ }
20
+ : null;
21
+ },
22
+
23
+ async createUser(
24
+ id: string,
25
+ username: string,
26
+ displayName: string,
27
+ ): Promise<User> {
28
+ await env.DB.prepare(
29
+ "INSERT INTO users (id, username, display_name) VALUES (?, ?, ?)",
30
+ )
31
+ .bind(id, username, displayName)
32
+ .run();
33
+ return { id, username, displayName };
34
+ },
35
+
36
+ // Passkey 相关
37
+ async getPasskeyById(credentialID: string): Promise<Passkey | null> {
38
+ const stmt = env.DB.prepare("SELECT * FROM passkeys WHERE id = ?").bind(
39
+ credentialID,
40
+ );
41
+ const row = await stmt.first();
42
+ if (!row) return null;
43
+ return {
44
+ id: row.id as string,
45
+ userId: row.user_id as string,
46
+ publicKey: row.public_key as string,
47
+ counter: row.counter as number,
48
+ deviceType: row.device_type as "singleDevice" | "multiDevice",
49
+ backedUp: Boolean(row.backed_up),
50
+ transports: row.transports
51
+ ? JSON.parse(row.transports as string)
52
+ : undefined,
53
+ };
54
+ },
55
+
56
+ async savePasskey(passkey: Passkey): Promise<void> {
57
+ const transportsStr = passkey.transports
58
+ ? JSON.stringify(passkey.transports)
59
+ : null;
60
+ await env.DB.prepare(
61
+ `
62
+ INSERT INTO passkeys (id, user_id, public_key, counter, device_type, backed_up, transports)
63
+ VALUES (?, ?, ?, ?, ?, ?, ?)
64
+ ON CONFLICT(id) DO UPDATE SET
65
+ counter = excluded.counter,
66
+ backed_up = excluded.backed_up,
67
+ transports = excluded.transports
68
+ `,
69
+ )
70
+ .bind(
71
+ passkey.id,
72
+ passkey.userId,
73
+ passkey.publicKey,
74
+ passkey.counter,
75
+ passkey.deviceType,
76
+ passkey.backedUp ? 1 : 0,
77
+ transportsStr,
78
+ )
79
+ .run();
80
+ },
81
+
82
+ async listPasskeysByUserId(userId: string): Promise<Passkey[]> {
83
+ const stmt = env.DB.prepare(
84
+ "SELECT * FROM passkeys WHERE user_id = ?",
85
+ ).bind(userId);
86
+ const rows = await stmt.all();
87
+ return rows.results.map((row: any) => ({
88
+ id: row.id,
89
+ userId: row.user_id,
90
+ publicKey: row.public_key,
91
+ counter: row.counter,
92
+ deviceType: row.device_type,
93
+ backedUp: Boolean(row.backed_up),
94
+ transports: row.transports ? JSON.parse(row.transports) : undefined,
95
+ }));
96
+ },
97
+ };
98
+ };
@@ -0,0 +1,53 @@
1
+ import {
2
+ AuthenticatorTransport,
3
+ generateRegistrationOptions,
4
+ } from "@simplewebauthn/server";
5
+ import { Context } from "hono";
6
+ import { Buffer } from "node:buffer";
7
+ import { createDatabaseQuery } from "./db";
8
+
9
+ const rpID = "localhost"; // 主机名,不含协议
10
+
11
+ export const registerOptions = async (c: Context) => {
12
+ const { username, displayName } = await c.req.json();
13
+ const db = createDatabaseQuery(c);
14
+
15
+ if (!username || !displayName) {
16
+ return c.json({ error: "Missing username or displayName" }, 400);
17
+ }
18
+
19
+ let user = await db.getUserByUsername(username);
20
+ if (!user) {
21
+ // 创建新用户(实际中可能需先验证邮箱等)
22
+ const crypto = c.env.crypto || globalThis.crypto;
23
+ const userId = crypto.randomUUID();
24
+ user = await db.createUser(userId, username, displayName);
25
+ }
26
+
27
+ const existingPasskeys = await db.listPasskeysByUserId(user.id);
28
+
29
+ const options = await generateRegistrationOptions({
30
+ rpName: "bbki.ng",
31
+ rpID,
32
+ userID: Buffer.from(user.id),
33
+ userName: user.username,
34
+ userDisplayName: user.displayName,
35
+ timeout: 60000,
36
+ attestationType: "none",
37
+ excludeCredentials: existingPasskeys.map((pk) => ({
38
+ id: atob(pk.id),
39
+ transports: pk.transports as AuthenticatorTransport[],
40
+ })),
41
+ authenticatorSelection: {
42
+ residentKey: "discouraged",
43
+ userVerification: "discouraged",
44
+ },
45
+ supportedAlgorithmIDs: [-7, -257], // ES256, RS256
46
+ });
47
+
48
+ c.env.KV.put(username, options.challenge, {
49
+ expirationTtl: 300, // 5 minutes
50
+ });
51
+
52
+ return c.json(options);
53
+ };
@@ -0,0 +1,71 @@
1
+ import {
2
+ RegistrationResponseJSON,
3
+ verifyRegistrationResponse,
4
+ } from "@simplewebauthn/server";
5
+ import { Context } from "hono";
6
+ import { createDatabaseQuery } from "./db";
7
+
8
+ const rpID = "localhost"; // 主机名,不含协议
9
+ const origin = `http://${rpID}:8787`; // 完整的来源 URL,含协议和端口
10
+
11
+ export const verify = async (c: Context) => {
12
+ const {
13
+ username,
14
+ response,
15
+ }: { username: string; response: RegistrationResponseJSON } =
16
+ await c.req.json();
17
+ const db = createDatabaseQuery(c);
18
+
19
+ const user = await db.getUserByUsername(username);
20
+ if (!user) {
21
+ return c.json({ error: "User not found" }, 404);
22
+ }
23
+
24
+ const challenge = c.env.KV.get(username);
25
+
26
+ let verification;
27
+ try {
28
+ verification = await verifyRegistrationResponse({
29
+ response,
30
+ expectedChallenge: challenge,
31
+ expectedOrigin: origin,
32
+ expectedRPID: rpID,
33
+ requireUserVerification: false,
34
+ });
35
+ } catch (error) {
36
+ console.error("Registration verification failed:", error);
37
+ return c.json({ error: "Registration failed" }, 400);
38
+ }
39
+
40
+ const { verified, registrationInfo } = verification;
41
+
42
+ if (!verified || !registrationInfo) {
43
+ return c.json({ error: "Registration not verified" }, 400);
44
+ }
45
+
46
+ const { credential, credentialDeviceType, credentialBackedUp } =
47
+ registrationInfo;
48
+
49
+ // 将 credentialID 转为 base64url 字符串作为主键
50
+ const credentialIDStr = credential.id
51
+ .replace(/\+/g, "-")
52
+ .replace(/\//g, "_")
53
+ .replace(/=/g, "");
54
+
55
+ const publicKeyStr = btoa(String.fromCharCode(...credential.publicKey))
56
+ .replace(/\+/g, "-")
57
+ .replace(/\//g, "_")
58
+ .replace(/=/g, "");
59
+
60
+ await db.savePasskey({
61
+ id: credentialIDStr,
62
+ userId: user.id,
63
+ publicKey: publicKeyStr,
64
+ counter: credential.counter,
65
+ deviceType: credentialDeviceType,
66
+ backedUp: credentialBackedUp,
67
+ transports: credential.transports,
68
+ });
69
+
70
+ return c.json({ verified: true });
71
+ };
@@ -0,0 +1,25 @@
1
+ import { Context } from "hono";
2
+
3
+ export const addComment = async (c: Context) => {
4
+ const { articleId, author, content } = await c.req.json();
5
+
6
+ try {
7
+ let { results } = await c.env.DB.prepare(
8
+ "INSERT INTO comment (article_id, author, content, created_at) VALUES (?, ?, ?, ?);",
9
+ )
10
+ .bind(articleId, author, content, new Date().toISOString())
11
+ .run();
12
+
13
+ return c.json({
14
+ status: "success",
15
+ message: "Comment added successfully",
16
+ results,
17
+ });
18
+ } catch (error: any) {
19
+ return c.json({
20
+ status: "error",
21
+ message: "Failed to add comment",
22
+ error: error.message,
23
+ });
24
+ }
25
+ };
@@ -0,0 +1,20 @@
1
+ import { Context } from "hono";
2
+
3
+ export const listStreaming = async (c: Context) => {
4
+ try {
5
+ const { results } = await c.env.DB.prepare(
6
+ "SELECT id, author, content, type, created_at as createdAt FROM streaming ORDER BY created_at DESC LIMIT 100"
7
+ ).all();
8
+
9
+ return c.json({
10
+ status: "success",
11
+ data: results,
12
+ });
13
+ } catch (error: any) {
14
+ return c.json({
15
+ status: "error",
16
+ message: "Failed to fetch streaming",
17
+ error: error.message,
18
+ }, 500);
19
+ }
20
+ };
package/src/index.ts CHANGED
@@ -1,45 +1,15 @@
1
- import { Hono } from "hono";
2
- import type { D1Database } from "@cloudflare/workers-types";
3
- import { cors } from "hono/cors";
1
+ import app from "./config/app.config";
4
2
 
5
- type Bindings = {
6
- DB: D1Database;
7
- };
3
+ import { commentRouter } from "./routes/comment.routes";
4
+ import { authRoutes } from "./routes/authn.routes";
5
+ import { streamingRouter } from "./routes/streaming.routes";
8
6
 
9
- const app = new Hono<{ Bindings: Bindings }>();
7
+ app.route("comment", commentRouter);
8
+ app.route("auth", authRoutes);
9
+ app.route("streaming", streamingRouter);
10
10
 
11
11
  app.get("/", (c) => {
12
12
  return c.text("Hello Hono!");
13
13
  });
14
14
 
15
- app.post(
16
- "/comment/add",
17
- cors({
18
- origin: ["https://bbki.ng"],
19
- }),
20
- async (c) => {
21
- const { articleId, author, content } = await c.req.json();
22
-
23
- try {
24
- let { results } = await c.env.DB.prepare(
25
- "INSERT INTO comment (article_id, author, content, created_at) VALUES (?, ?, ?, ?);",
26
- )
27
- .bind(articleId, author, content, new Date().toISOString())
28
- .run();
29
-
30
- return c.json({
31
- status: "success",
32
- message: "Comment added successfully",
33
- results,
34
- });
35
- } catch (error: any) {
36
- return c.json({
37
- status: "error",
38
- message: "Failed to add comment",
39
- error: error.message,
40
- });
41
- }
42
- },
43
- );
44
-
45
15
  export default app;
@@ -0,0 +1,10 @@
1
+ import { Hono } from "hono";
2
+ import { registerOptions } from "../controllers/authn/register.controller";
3
+ import { verify } from "../controllers/authn/verify.controller";
4
+
5
+ const authRoutes = new Hono();
6
+
7
+ authRoutes.post("/register/options", registerOptions);
8
+ authRoutes.post("/register/verify", verify);
9
+
10
+ export { authRoutes };
@@ -0,0 +1,8 @@
1
+ import { Hono } from "hono";
2
+ import { addComment } from "../controllers/comment/add.controller";
3
+
4
+ const commentRouter = new Hono();
5
+
6
+ commentRouter.post("/add", addComment);
7
+
8
+ export { commentRouter };
@@ -0,0 +1,8 @@
1
+ import { Hono } from "hono";
2
+ import { listStreaming } from "../controllers/streaming/list.controller";
3
+
4
+ const streamingRouter = new Hono();
5
+
6
+ streamingRouter.get("/", listStreaming);
7
+
8
+ export { streamingRouter };
@@ -0,0 +1,23 @@
1
+ export type User = {
2
+ id: string;
3
+ username: string;
4
+ displayName: string;
5
+ };
6
+
7
+ export type Passkey = {
8
+ id: string;
9
+ userId: string;
10
+ publicKey: string; // base64url
11
+ counter: number;
12
+ deviceType: "singleDevice" | "multiDevice";
13
+ backedUp: boolean;
14
+ transports?: string[]; // 存为 JSON 字符串
15
+ };
16
+
17
+ export type Streaming = {
18
+ id: string;
19
+ author: string;
20
+ content: string;
21
+ type?: string;
22
+ createdAt: string;
23
+ };
package/wrangler.jsonc CHANGED
@@ -28,6 +28,17 @@
28
28
  "database_id": "e938a347-b3be-4d60-9217-52f209ab7bb4",
29
29
  },
30
30
  ],
31
+ // Add this to your wrangler.jsonc
32
+ "kv_namespaces": [
33
+ {
34
+ "binding": "KV",
35
+ "id": "91dd9f607aae46e5829080d1c2aa3baa",
36
+
37
+ // Optional: preview_id used when running `wrangler dev` for local dev
38
+ "preview_id": "BBKING_KV_NAMESPACE_PREVIEW_ID",
39
+ },
40
+ ],
41
+ "compatibility_flags": ["nodejs_compat"],
31
42
  // "ai": {
32
43
  // "binding": "AI"
33
44
  // },