@bbki.ng/backend 0.0.1 → 0.2.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/.dev.vars.example +4 -0
- package/CHANGELOG.md +4 -0
- package/package.json +4 -2
- package/src/config/app.config.ts +19 -0
- package/src/controllers/authn/db.ts +98 -0
- package/src/controllers/authn/register.controller.ts +53 -0
- package/src/controllers/authn/verify.controller.ts +71 -0
- package/src/controllers/comment/add.controller.ts +25 -0
- package/src/controllers/streaming/add.controller.ts +93 -0
- package/src/controllers/streaming/list.controller.ts +20 -0
- package/src/index.ts +7 -37
- package/src/routes/authn.routes.ts +10 -0
- package/src/routes/comment.routes.ts +8 -0
- package/src/routes/streaming.routes.ts +10 -0
- package/src/types/index.ts +23 -0
- package/wrangler.jsonc +14 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bbki.ng/backend",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.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
|
-
"
|
|
11
|
+
"@types/node": "25.0.3",
|
|
12
|
+
"wrangler": "^4.58.0"
|
|
11
13
|
},
|
|
12
14
|
"scripts": {
|
|
13
15
|
"dev": "wrangler dev",
|
|
@@ -0,0 +1,19 @@
|
|
|
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
|
+
API_KEY: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const app = new Hono<{ Bindings: Bindings }>();
|
|
11
|
+
|
|
12
|
+
app.use(
|
|
13
|
+
"*",
|
|
14
|
+
cors({
|
|
15
|
+
origin: ["https://bbki.ng"],
|
|
16
|
+
}),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
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,93 @@
|
|
|
1
|
+
import { Context } from "hono";
|
|
2
|
+
import { HTTPException } from "hono/http-exception";
|
|
3
|
+
|
|
4
|
+
interface AddStreamingRequest {
|
|
5
|
+
author?: string;
|
|
6
|
+
content: string;
|
|
7
|
+
type?: 'note' | 'article' | 'link' | 'image';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MAX_CONTENT_LENGTH = 50000;
|
|
11
|
+
const ALLOWED_TYPES = ['note', 'article', 'link', 'image'] as const;
|
|
12
|
+
|
|
13
|
+
const timingSafeEqual = (a: string, b: string): boolean => {
|
|
14
|
+
if (a.length !== b.length) return false;
|
|
15
|
+
let result = 0;
|
|
16
|
+
for (let i = 0; i < a.length; i++) {
|
|
17
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
18
|
+
}
|
|
19
|
+
return result === 0;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const addStreaming = async (c: Context) => {
|
|
23
|
+
try {
|
|
24
|
+
// 1. 认证(Header 方式)
|
|
25
|
+
const apiKey = c.req.header('x-api-key');
|
|
26
|
+
if (!apiKey || !timingSafeEqual(apiKey, c.env.API_KEY)) {
|
|
27
|
+
throw new HTTPException(401, { message: "Invalid API key" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. 解析与验证
|
|
31
|
+
const body = await c.req.json<AddStreamingRequest>();
|
|
32
|
+
|
|
33
|
+
if (!body.content || typeof body.content !== "string") {
|
|
34
|
+
throw new HTTPException(400, { message: "Content is required" });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const content = body.content.trim();
|
|
38
|
+
if (content.length === 0) {
|
|
39
|
+
throw new HTTPException(400, { message: "Content cannot be empty" });
|
|
40
|
+
}
|
|
41
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
42
|
+
throw new HTTPException(413, { message: `Content exceeds ${MAX_CONTENT_LENGTH} chars` });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const author = (body.author ?? 'bbki.ng').trim();
|
|
46
|
+
if (author.length > 50) {
|
|
47
|
+
throw new HTTPException(400, { message: "Author too long" });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const type = body.type?.trim();
|
|
51
|
+
if (type && !ALLOWED_TYPES.includes(type as typeof ALLOWED_TYPES[number])) {
|
|
52
|
+
throw new HTTPException(400, { message: "Invalid type" });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3. 数据库检查
|
|
56
|
+
if (!c.env?.DB) {
|
|
57
|
+
throw new HTTPException(503, { message: "Database unavailable" });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 4. 写入
|
|
61
|
+
const id = crypto.randomUUID();
|
|
62
|
+
const createdAt = new Date().toISOString();
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await c.env.DB.prepare(
|
|
66
|
+
"INSERT INTO streaming (id, author, content, type, created_at) VALUES (?, ?, ?, ?, ?)"
|
|
67
|
+
)
|
|
68
|
+
.bind(id, author, content, type || null, createdAt)
|
|
69
|
+
.run();
|
|
70
|
+
} catch (dbError) {
|
|
71
|
+
if (dbError instanceof Error && dbError.message.includes('UNIQUE')) {
|
|
72
|
+
throw new HTTPException(409, { message: "Duplicate entry" });
|
|
73
|
+
}
|
|
74
|
+
throw dbError;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return c.json({
|
|
78
|
+
status: "success",
|
|
79
|
+
data: { id, author, content, type, createdAt }
|
|
80
|
+
}, 201);
|
|
81
|
+
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error instanceof HTTPException) {
|
|
84
|
+
return c.json({ status: "error", message: error.message }, error.status);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.error('Unexpected error:', error);
|
|
88
|
+
return c.json({
|
|
89
|
+
status: "error",
|
|
90
|
+
message: "Internal server error"
|
|
91
|
+
}, 500);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
@@ -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
|
|
2
|
-
import type { D1Database } from "@cloudflare/workers-types";
|
|
3
|
-
import { cors } from "hono/cors";
|
|
1
|
+
import app from "./config/app.config";
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
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,10 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { listStreaming } from "../controllers/streaming/list.controller";
|
|
3
|
+
import { addStreaming } from "../controllers/streaming/add.controller";
|
|
4
|
+
|
|
5
|
+
const streamingRouter = new Hono();
|
|
6
|
+
|
|
7
|
+
streamingRouter.get("/", listStreaming);
|
|
8
|
+
streamingRouter.post("/", addStreaming);
|
|
9
|
+
|
|
10
|
+
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,20 @@
|
|
|
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"],
|
|
42
|
+
"vars": {
|
|
43
|
+
"API_KEY": "your-secret-api-key-change-in-production"
|
|
44
|
+
},
|
|
31
45
|
// "ai": {
|
|
32
46
|
// "binding": "AI"
|
|
33
47
|
// },
|