@zhimakechuang/game-sdk 1.1.0 → 1.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/dist/index.d.mts +30 -24
- package/dist/index.d.ts +30 -24
- package/dist/index.global.js +28 -28
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +28 -28
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +28 -28
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -6,25 +6,27 @@ type Platform = 'wx' | 'tt' | 'guest';
|
|
|
6
6
|
/** SDK 配置 */
|
|
7
7
|
interface GameSdkConfig {
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - 游戏运行阶段由平台构建时注入到 __GAME_CONFIG__
|
|
9
|
+
* 模板 ID(模板开发者必填)。
|
|
10
|
+
* 标识当前开发的是哪个模板,由平台在模板创建时生成。
|
|
12
11
|
*/
|
|
13
|
-
|
|
14
|
-
/** api-server 根地址(如 https://api.example.com) */
|
|
15
|
-
baseUrl: string;
|
|
12
|
+
templateId: string;
|
|
16
13
|
/**
|
|
17
14
|
* 应用标识(AccessKey),用于鉴权。
|
|
18
|
-
*
|
|
15
|
+
* 由平台颁发给入驻开发者,必须在 SDK 初始化时传入。
|
|
16
|
+
* 未入驻的开发者无法获取,从而阻止未授权调用。
|
|
19
17
|
*/
|
|
20
|
-
accessKey
|
|
18
|
+
accessKey: string;
|
|
21
19
|
/**
|
|
22
20
|
* 应用密钥(SecretKey),用于请求签名。
|
|
23
|
-
*
|
|
21
|
+
* 由平台颁发给入驻开发者,必须在 SDK 初始化时传入。
|
|
24
22
|
* ⚠️ 此值不会出现在网络请求中,仅用于本地计算签名。
|
|
25
23
|
*/
|
|
26
|
-
secretKey
|
|
27
|
-
/**
|
|
24
|
+
secretKey: string;
|
|
25
|
+
/**
|
|
26
|
+
* 模拟模式(不连服务端,返回假数据,用于模板开发)。
|
|
27
|
+
* mock 模式下仍需传入 templateId/accessKey/secretKey,
|
|
28
|
+
* 但不会发起真实网络请求。
|
|
29
|
+
*/
|
|
28
30
|
mock?: boolean;
|
|
29
31
|
}
|
|
30
32
|
interface LoginOptions {
|
|
@@ -87,27 +89,31 @@ interface SaveEntry {
|
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
/**
|
|
90
|
-
* @
|
|
92
|
+
* @zhimakechuang/game-sdk
|
|
91
93
|
*
|
|
92
|
-
*
|
|
94
|
+
* game-factory 平台专属游戏 SDK,封装 game-server 所有 HTTP 接口。
|
|
93
95
|
*
|
|
94
|
-
*
|
|
95
|
-
* import { createGameSdk } from '@
|
|
96
|
-
* const sdk = createGameSdk({
|
|
96
|
+
* 用法(模板开发 + 生产运行统一入口):
|
|
97
|
+
* import { createGameSdk } from '@zhimakechuang/game-sdk';
|
|
98
|
+
* const sdk = createGameSdk({
|
|
99
|
+
* templateId: 'tpl_xxx', // 模板 ID
|
|
100
|
+
* accessKey: 'ak_tpl_xxx', // 平台入驻后颁发的凭证
|
|
101
|
+
* secretKey: 'sk_tpl_xxx', // 平台入驻后颁发的密钥
|
|
102
|
+
* });
|
|
97
103
|
* await sdk.auth.login({ code: 'xxx', platform: 'wx' });
|
|
98
104
|
* await sdk.score.submit({ score: 1000 });
|
|
99
105
|
*
|
|
100
106
|
* 用法(CDN):
|
|
101
|
-
* <script src="https://
|
|
102
|
-
* const sdk = GameSDK.createGameSdk({
|
|
107
|
+
* <script src="https://unpkg.com/@zhimakechuang/game-sdk/dist/index.global.js"></script>
|
|
108
|
+
* const sdk = GameSDK.createGameSdk({ templateId, accessKey, secretKey });
|
|
103
109
|
*
|
|
104
|
-
*
|
|
105
|
-
* const sdk = createGameSdk({
|
|
110
|
+
* 用法(模拟模式,模板开发调试,不连服务端):
|
|
111
|
+
* const sdk = createGameSdk({ templateId, accessKey, secretKey, mock: true });
|
|
106
112
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* SDK
|
|
110
|
-
*
|
|
113
|
+
* 鉴权说明:
|
|
114
|
+
* - accessKey/secretKey 必须传入,只有平台入驻开发者才能获取
|
|
115
|
+
* - gameId 由平台构建时注入到 __GAME_CONFIG__,SDK 自动读取,开发者无需传入
|
|
116
|
+
* - baseUrl 内置默认值,指向平台 API 服务
|
|
111
117
|
*/
|
|
112
118
|
|
|
113
119
|
declare class GameSdkError extends Error {
|
package/dist/index.d.ts
CHANGED
|
@@ -6,25 +6,27 @@ type Platform = 'wx' | 'tt' | 'guest';
|
|
|
6
6
|
/** SDK 配置 */
|
|
7
7
|
interface GameSdkConfig {
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - 游戏运行阶段由平台构建时注入到 __GAME_CONFIG__
|
|
9
|
+
* 模板 ID(模板开发者必填)。
|
|
10
|
+
* 标识当前开发的是哪个模板,由平台在模板创建时生成。
|
|
12
11
|
*/
|
|
13
|
-
|
|
14
|
-
/** api-server 根地址(如 https://api.example.com) */
|
|
15
|
-
baseUrl: string;
|
|
12
|
+
templateId: string;
|
|
16
13
|
/**
|
|
17
14
|
* 应用标识(AccessKey),用于鉴权。
|
|
18
|
-
*
|
|
15
|
+
* 由平台颁发给入驻开发者,必须在 SDK 初始化时传入。
|
|
16
|
+
* 未入驻的开发者无法获取,从而阻止未授权调用。
|
|
19
17
|
*/
|
|
20
|
-
accessKey
|
|
18
|
+
accessKey: string;
|
|
21
19
|
/**
|
|
22
20
|
* 应用密钥(SecretKey),用于请求签名。
|
|
23
|
-
*
|
|
21
|
+
* 由平台颁发给入驻开发者,必须在 SDK 初始化时传入。
|
|
24
22
|
* ⚠️ 此值不会出现在网络请求中,仅用于本地计算签名。
|
|
25
23
|
*/
|
|
26
|
-
secretKey
|
|
27
|
-
/**
|
|
24
|
+
secretKey: string;
|
|
25
|
+
/**
|
|
26
|
+
* 模拟模式(不连服务端,返回假数据,用于模板开发)。
|
|
27
|
+
* mock 模式下仍需传入 templateId/accessKey/secretKey,
|
|
28
|
+
* 但不会发起真实网络请求。
|
|
29
|
+
*/
|
|
28
30
|
mock?: boolean;
|
|
29
31
|
}
|
|
30
32
|
interface LoginOptions {
|
|
@@ -87,27 +89,31 @@ interface SaveEntry {
|
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
/**
|
|
90
|
-
* @
|
|
92
|
+
* @zhimakechuang/game-sdk
|
|
91
93
|
*
|
|
92
|
-
*
|
|
94
|
+
* game-factory 平台专属游戏 SDK,封装 game-server 所有 HTTP 接口。
|
|
93
95
|
*
|
|
94
|
-
*
|
|
95
|
-
* import { createGameSdk } from '@
|
|
96
|
-
* const sdk = createGameSdk({
|
|
96
|
+
* 用法(模板开发 + 生产运行统一入口):
|
|
97
|
+
* import { createGameSdk } from '@zhimakechuang/game-sdk';
|
|
98
|
+
* const sdk = createGameSdk({
|
|
99
|
+
* templateId: 'tpl_xxx', // 模板 ID
|
|
100
|
+
* accessKey: 'ak_tpl_xxx', // 平台入驻后颁发的凭证
|
|
101
|
+
* secretKey: 'sk_tpl_xxx', // 平台入驻后颁发的密钥
|
|
102
|
+
* });
|
|
97
103
|
* await sdk.auth.login({ code: 'xxx', platform: 'wx' });
|
|
98
104
|
* await sdk.score.submit({ score: 1000 });
|
|
99
105
|
*
|
|
100
106
|
* 用法(CDN):
|
|
101
|
-
* <script src="https://
|
|
102
|
-
* const sdk = GameSDK.createGameSdk({
|
|
107
|
+
* <script src="https://unpkg.com/@zhimakechuang/game-sdk/dist/index.global.js"></script>
|
|
108
|
+
* const sdk = GameSDK.createGameSdk({ templateId, accessKey, secretKey });
|
|
103
109
|
*
|
|
104
|
-
*
|
|
105
|
-
* const sdk = createGameSdk({
|
|
110
|
+
* 用法(模拟模式,模板开发调试,不连服务端):
|
|
111
|
+
* const sdk = createGameSdk({ templateId, accessKey, secretKey, mock: true });
|
|
106
112
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* SDK
|
|
110
|
-
*
|
|
113
|
+
* 鉴权说明:
|
|
114
|
+
* - accessKey/secretKey 必须传入,只有平台入驻开发者才能获取
|
|
115
|
+
* - gameId 由平台构建时注入到 __GAME_CONFIG__,SDK 自动读取,开发者无需传入
|
|
116
|
+
* - baseUrl 内置默认值,指向平台 API 服务
|
|
111
117
|
*/
|
|
112
118
|
|
|
113
119
|
declare class GameSdkError extends Error {
|
package/dist/index.global.js
CHANGED
|
@@ -191,21 +191,27 @@ var GameSDK = (() => {
|
|
|
191
191
|
}
|
|
192
192
|
};
|
|
193
193
|
function createGameSdk(config) {
|
|
194
|
-
|
|
195
|
-
if (!runtimeConfig.mock && !runtimeConfig.gameId) {
|
|
194
|
+
if (!config.templateId) {
|
|
196
195
|
throw new GameSdkError(
|
|
197
|
-
"\u7F3A\u5C11
|
|
196
|
+
"\u7F3A\u5C11 templateId\uFF1A\u8BF7\u5728\u5E73\u53F0\u521B\u5EFA\u6A21\u677F\u540E\u83B7\u53D6\u6A21\u677F ID",
|
|
198
197
|
400
|
|
199
198
|
);
|
|
200
199
|
}
|
|
201
|
-
if (!
|
|
200
|
+
if (!config.accessKey || !config.secretKey) {
|
|
202
201
|
throw new GameSdkError(
|
|
203
|
-
"\u7F3A\u5C11 accessKey/secretKey\uFF1A\u8BF7\u786E\u8BA4\
|
|
202
|
+
"\u7F3A\u5C11 accessKey/secretKey\uFF1A\u8BF7\u786E\u8BA4\u5DF2\u5728\u5E73\u53F0\u5165\u9A7B\u5E76\u83B7\u53D6\u5F00\u53D1\u8005\u51ED\u8BC1",
|
|
204
203
|
400
|
|
205
204
|
);
|
|
206
205
|
}
|
|
207
|
-
const
|
|
208
|
-
|
|
206
|
+
const { gameId, baseUrl } = resolveInjectedConfig();
|
|
207
|
+
if (!config.mock && !gameId) {
|
|
208
|
+
throw new GameSdkError(
|
|
209
|
+
"\u7F3A\u5C11 gameId\uFF1A\u8BF7\u786E\u8BA4\u6E38\u620F\u5DF2\u901A\u8FC7\u5E73\u53F0\u6784\u5EFA\uFF0C\u6216\u4F7F\u7528 mock \u6A21\u5F0F\u8FDB\u884C\u6A21\u677F\u5F00\u53D1",
|
|
210
|
+
400
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const apiUrl = gameId ? `${baseUrl}/game-api/${gameId}` : "";
|
|
214
|
+
const tokenKey = `gf_token_${gameId ?? "mock"}`;
|
|
209
215
|
let token = storageGet(tokenKey);
|
|
210
216
|
function getToken() {
|
|
211
217
|
return token;
|
|
@@ -216,19 +222,17 @@ var GameSDK = (() => {
|
|
|
216
222
|
else storageRemove(tokenKey);
|
|
217
223
|
}
|
|
218
224
|
async function call(method, path, body) {
|
|
219
|
-
if (
|
|
225
|
+
if (config.mock) {
|
|
220
226
|
return mockResponse(method, path, body);
|
|
221
227
|
}
|
|
222
228
|
const headers = {};
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
Object.assign(headers, signHeaders);
|
|
231
|
-
}
|
|
229
|
+
const signHeaders = buildSignHeaders(
|
|
230
|
+
method,
|
|
231
|
+
path,
|
|
232
|
+
body,
|
|
233
|
+
{ accessKey: config.accessKey, secretKey: config.secretKey }
|
|
234
|
+
);
|
|
235
|
+
Object.assign(headers, signHeaders);
|
|
232
236
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
233
237
|
const res = await request({
|
|
234
238
|
url: `${apiUrl}${path}`,
|
|
@@ -275,20 +279,16 @@ var GameSDK = (() => {
|
|
|
275
279
|
};
|
|
276
280
|
return sdk;
|
|
277
281
|
}
|
|
278
|
-
function
|
|
279
|
-
let injected = {};
|
|
282
|
+
function resolveInjectedConfig() {
|
|
280
283
|
try {
|
|
281
|
-
injected = typeof __GAME_CONFIG__ !== "undefined" ? __GAME_CONFIG__ : {};
|
|
284
|
+
const injected = typeof __GAME_CONFIG__ !== "undefined" ? __GAME_CONFIG__ : {};
|
|
285
|
+
return {
|
|
286
|
+
gameId: injected.gameId,
|
|
287
|
+
baseUrl: injected.baseUrl ?? ""
|
|
288
|
+
};
|
|
282
289
|
} catch {
|
|
283
|
-
|
|
290
|
+
return { gameId: void 0, baseUrl: "" };
|
|
284
291
|
}
|
|
285
|
-
return {
|
|
286
|
-
gameId: config.gameId ?? injected.gameId,
|
|
287
|
-
baseUrl: config.baseUrl ?? injected.baseUrl ?? "",
|
|
288
|
-
accessKey: config.accessKey ?? injected.accessKey,
|
|
289
|
-
secretKey: config.secretKey ?? injected.secretKey,
|
|
290
|
-
mock: config.mock
|
|
291
|
-
};
|
|
292
292
|
}
|
|
293
293
|
var mockScore = 0;
|
|
294
294
|
var mockSaves = {};
|
package/dist/index.global.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/platform.ts","../src/sign.ts"],"sourcesContent":["/**\n * @game-factory/game-sdk\n *\n * 通用游戏 SDK,封装 game-server-shared 所有 HTTP 接口。\n *\n * 用法(工程化):\n * import { createGameSdk } from '@game-factory/game-sdk';\n * const sdk = createGameSdk({ baseUrl: 'https://api.example.com' });\n * await sdk.auth.login({ code: 'xxx', platform: 'wx' });\n * await sdk.score.submit({ score: 1000 });\n *\n * 用法(CDN):\n * <script src=\"https://cdn.example.com/game-sdk/index.umd.js\"></script>\n * const sdk = GameSDK.createGameSdk({ baseUrl: '...' });\n *\n * 用法(模拟模式,不连服务端):\n * const sdk = createGameSdk({ baseUrl: '', mock: true });\n *\n * 凭证说明:\n * gameId / accessKey / secretKey 由平台在构建时注入到 __GAME_CONFIG__,\n * SDK 自动读取,开发者无需手动传入。\n * 模板开发阶段使用 mock 模式,不需要凭证。\n */\n\nimport {\n GameSdkConfig, LoginOptions, LoginResult, PlayerProfile,\n SubmitScoreOptions, SubmitScoreResult,\n LeaderboardEntry, MyRank,\n} from './types';\nimport { request, storageGet, storageSet, storageRemove } from './platform';\nimport { buildSignHeaders } from './sign';\n\n// 运行时由平台构建时注入的配置(esbuild define)\ndeclare const __GAME_CONFIG__: {\n gameId?: string;\n accessKey?: string;\n secretKey?: string;\n baseUrl?: string;\n} | undefined;\n\n// re-export 所有类型,供外部使用\nexport * from './types';\n\n// ─── 错误 ──────────────────────────────────────────────\n\nexport class GameSdkError extends Error {\n constructor(\n message: string,\n public status: number,\n public data?: any,\n ) {\n super(message);\n this.name = 'GameSdkError';\n }\n}\n\n// ─── SDK 接口定义 ───────────────────────────────────────\n\nexport interface GameSdk {\n /** 身份与登录 */\n auth: {\n /** 登录(微信/抖音/访客),成功后自动持久化 token */\n login(opts: LoginOptions): Promise<LoginResult>;\n /** 获取当前登录玩家信息(需先 login) */\n me(): Promise<PlayerProfile>;\n /** 登出,清除本地 token */\n logout(): void;\n /** 获取当前 token(用于调试) */\n getToken(): string | null;\n };\n /** 分数 */\n score: {\n /** 提交分数(需登录) */\n submit(opts: SubmitScoreOptions): Promise<SubmitScoreResult>;\n };\n /** 排行榜 */\n leaderboard: {\n /** 获取 Top N(匿名可访问,limit 默认 100) */\n top(limit?: number): Promise<LeaderboardEntry[]>;\n /** 获取当前玩家排名(需登录) */\n myRank(): Promise<MyRank>;\n /** 获取附近排名(需登录,range 默认 ±5) */\n nearby(range?: number): Promise<LeaderboardEntry[]>;\n };\n /** 玩家档案 */\n player: {\n /** 获取当前玩家档案(需登录) */\n getProfile(): Promise<PlayerProfile>;\n /** 更新档案(昵称/头像/扩展字段) */\n updateProfile(patch: {\n nickname?: string;\n avatarUrl?: string;\n extra?: Record<string, any>;\n }): Promise<PlayerProfile>;\n };\n /** 云存档(KV) */\n save: {\n /** 保存数据到指定 key */\n set(key: string, value: Record<string, any>): Promise<{ success: boolean; key: string; value: Record<string, any> }>;\n /** 读取指定 key 的数据 */\n get(key: string): Promise<Record<string, any> | null>;\n /** 读取所有存档 */\n getAll(): Promise<Record<string, any>>;\n /** 删除指定 key */\n remove(key: string): Promise<{ success: boolean; key: string }>;\n /** 清空所有存档 */\n clear(): Promise<{ success: boolean; deleted: number }>;\n };\n}\n\n// ─── 创建 SDK ───────────────────────────────────────────\n\nexport function createGameSdk(config: GameSdkConfig): GameSdk {\n // 合并 __GAME_CONFIG__ 注入的凭证(构建时注入,运行时自动读取)\n // 优先级:显式传入 > __GAME_CONFIG__ 注入\n const runtimeConfig = resolveRuntimeConfig(config);\n\n // 非 mock 模式必须有 gameId(游戏运行阶段由平台注入)\n if (!runtimeConfig.mock && !runtimeConfig.gameId) {\n throw new GameSdkError(\n '缺少 gameId:请确认游戏已通过平台构建,或使用 mock 模式进行模板开发',\n 400,\n );\n }\n\n // 非 mock 模式必须有凭证(游戏运行阶段由平台注入)\n if (!runtimeConfig.mock && (!runtimeConfig.accessKey || !runtimeConfig.secretKey)) {\n throw new GameSdkError(\n '缺少 accessKey/secretKey:请确认游戏已通过平台构建并颁发了模板级凭证',\n 400,\n );\n }\n\n const apiUrl = runtimeConfig.baseUrl\n ? `${runtimeConfig.baseUrl}/game-api/${runtimeConfig.gameId}`\n : '';\n const tokenKey = `gf_token_${runtimeConfig.gameId ?? 'mock'}`;\n\n // token 管理(从本地存储恢复)\n let token: string | null = storageGet(tokenKey);\n\n function getToken(): string | null {\n return token;\n }\n\n function setToken(t: string | null): void {\n token = t;\n if (t) storageSet(tokenKey, t);\n else storageRemove(tokenKey);\n }\n\n // ── 内部 HTTP 封装 ──────────────────────────────────\n async function call<T = any>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n body?: any,\n ): Promise<T> {\n if (runtimeConfig.mock) {\n return mockResponse<T>(method, path, body);\n }\n\n const headers: Record<string, string> = {};\n\n // 附加请求签名(HMAC-SHA256,防篡改 + 防重放)\n if (runtimeConfig.accessKey && runtimeConfig.secretKey) {\n const signHeaders = buildSignHeaders(\n method,\n path,\n body,\n { accessKey: runtimeConfig.accessKey, secretKey: runtimeConfig.secretKey },\n );\n Object.assign(headers, signHeaders);\n }\n\n // 附加玩家 token\n if (token) headers['Authorization'] = `Bearer ${token}`;\n\n const res = await request<T>({\n url: `${apiUrl}${path}`,\n method,\n headers,\n body,\n });\n\n if (res.status >= 400) {\n const msg = (res.data as any)?.message || `HTTP ${res.status}`;\n throw new GameSdkError(msg, res.status, res.data);\n }\n return res.data;\n }\n\n // ── 方法面 ──────────────────────────────────────────\n const sdk: GameSdk = {\n auth: {\n async login(opts: LoginOptions): Promise<LoginResult> {\n const result = await call<LoginResult>('POST', '/auth/login', opts);\n setToken(result.token);\n return result;\n },\n me: () => call<PlayerProfile>('GET', '/auth/me'),\n logout: () => setToken(null),\n getToken,\n },\n\n score: {\n submit: (opts: SubmitScoreOptions) => call<SubmitScoreResult>('POST', '/score/submit', opts),\n },\n\n leaderboard: {\n top: (limit?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard?limit=${limit || 100}`),\n myRank: () => call<MyRank>('GET', '/leaderboard/me'),\n nearby: (range?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard/nearby?range=${range || 5}`),\n },\n\n player: {\n getProfile: () => call<PlayerProfile>('GET', '/player/profile'),\n updateProfile: (patch) => call<PlayerProfile>('PATCH', '/player/profile', patch),\n },\n\n save: {\n set: (key, value) => call('POST', `/storage/${key}`, { value }),\n get: (key) => call('GET', `/storage/${key}`),\n getAll: () => call('GET', '/storage'),\n remove: (key) => call('DELETE', `/storage/${key}`),\n clear: () => call('DELETE', '/storage'),\n },\n };\n\n return sdk;\n}\n\n// ─── 运行时配置解析 ─────────────────────────────────────\n\n/**\n * 合并用户显式传入的配置与 __GAME_CONFIG__ 注入的配置。\n * 优先级:显式传入 > __GAME_CONFIG__ 注入\n *\n * __GAME_CONFIG__ 由平台构建时通过 esbuild define 注入:\n * define: { '__GAME_CONFIG__': JSON.stringify({ gameId, accessKey, secretKey, baseUrl }) }\n */\nfunction resolveRuntimeConfig(config: GameSdkConfig): GameSdkConfig {\n let injected: Partial<GameSdkConfig> = {};\n try {\n // __GAME_CONFIG__ 可能不存在(模板开发阶段 / CDN 直连)\n // eslint-disable-next-line no-undef\n injected = (typeof __GAME_CONFIG__ !== 'undefined' ? __GAME_CONFIG__ : {}) as any;\n } catch {\n // __GAME_CONFIG__ 未定义时 ReferenceError,忽略\n injected = {};\n }\n\n return {\n gameId: config.gameId ?? injected.gameId,\n baseUrl: config.baseUrl ?? injected.baseUrl ?? '',\n accessKey: config.accessKey ?? injected.accessKey,\n secretKey: config.secretKey ?? injected.secretKey,\n mock: config.mock,\n };\n}\n\n// ─── Mock 模式(模板开发不连服务端)──────────────────────\n\nlet mockScore = 0;\nlet mockSaves: Record<string, Record<string, any>> = {};\nlet mockToken: string | null = null;\n\nasync function mockResponse<T>(method: string, path: string, body: any): Promise<T> {\n await new Promise((r) => setTimeout(r, 50)); // 模拟网络延迟\n\n // auth\n if (path === '/auth/login') {\n mockToken = `mock_token_${Date.now()}`;\n const profile: PlayerProfile = {\n openId: 'mock_openid',\n platform: body?.platform || 'guest',\n nickname: body?.nickname || '测试玩家',\n bestScore: mockScore,\n playCount: 0,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n };\n return { token: mockToken, openId: 'mock_openid', isNew: true, profile } as T;\n }\n if (path === '/auth/me') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // score\n if (path === '/score/submit') {\n const prev = mockScore;\n mockScore = Math.max(mockScore, body?.score || 0);\n return {\n submitted: body?.score || 0,\n isHighScore: (body?.score || 0) > prev,\n previousBest: prev,\n bestScore: mockScore,\n rank: 1,\n } as T;\n }\n\n // leaderboard\n if (path.startsWith('/leaderboard?')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n if (path === '/leaderboard/me') {\n return { rank: 1, score: mockScore, totalPlayers: 1, playCount: 1 } as T;\n }\n if (path.startsWith('/leaderboard/nearby')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n\n // player\n if (path === '/player/profile' && method === 'GET') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // storage\n if (path.startsWith('/storage/') && method === 'POST') {\n const key = path.split('/storage/')[1];\n mockSaves[key] = body?.value || {};\n return { success: true, key, value: mockSaves[key] } as T;\n }\n if (path.startsWith('/storage/') && method === 'GET') {\n const key = path.split('/storage/')[1];\n return (mockSaves[key] || null) as T;\n }\n if (path === '/storage' && method === 'GET') {\n return mockSaves as T;\n }\n if (path.startsWith('/storage/') && method === 'DELETE') {\n const key = path.split('/storage/')[1];\n delete mockSaves[key];\n return { success: true, key } as T;\n }\n if (path === '/storage' && method === 'DELETE') {\n const count = Object.keys(mockSaves).length;\n mockSaves = {};\n return { success: true, deleted: count } as T;\n }\n\n return {} as T;\n}\n","/**\n * 平台适配层\n *\n * 自动检测运行环境(微信小游戏 / 抖音小游戏 / H5),\n * 封装 HTTP 请求和本地存储,对上层提供统一接口。\n */\n\n// 小游戏运行时的全局对象(微信/抖音),H5 下不存在\ndeclare const wx: any;\ndeclare const tt: any;\n\n// ─── 环境检测 ──────────────────────────────────────────\n\ntype Runtime = 'wx' | 'tt' | 'h5';\n\nfunction detectRuntime(): Runtime {\n if (typeof wx !== 'undefined' && typeof (wx as any).request === 'function') return 'wx';\n if (typeof tt !== 'undefined' && typeof (tt as any).request === 'function') return 'tt';\n return 'h5';\n}\n\nconst runtime = detectRuntime();\n\n// ─── HTTP 请求 ─────────────────────────────────────────\n\nexport interface HttpRequest {\n url: string;\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n headers?: Record<string, string>;\n body?: any;\n}\n\nexport interface HttpResponse<T = any> {\n status: number;\n data: T;\n}\n\nexport async function request<T = any>(req: HttpRequest): Promise<HttpResponse<T>> {\n if (runtime === 'wx' || runtime === 'tt') {\n return miniProgramRequest<T>(req);\n }\n return fetchRequest<T>(req);\n}\n\n/** 微信/抖音小游戏请求(wx.request / tt.request) */\nfunction miniProgramRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const api = (runtime === 'wx' ? wx : tt) as any;\n return new Promise((resolve, reject) => {\n api.request({\n url: req.url,\n method: req.method,\n header: req.headers || {},\n data: req.body,\n success: (res: any) => {\n resolve({\n status: res.statusCode,\n data: res.data as T,\n });\n },\n fail: (err: any) => {\n reject(new Error(err?.errMsg || 'request failed'));\n },\n });\n });\n}\n\n/** H5 fetch 请求 */\nasync function fetchRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const res = await fetch(req.url, {\n method: req.method,\n headers: { 'Content-Type': 'application/json', ...req.headers },\n body: req.body !== undefined ? JSON.stringify(req.body) : undefined,\n });\n const data = await res.json().catch(() => ({}));\n return { status: res.status, data: data as T };\n}\n\n// ─── 本地存储(token 持久化)────────────────────────────\n\nexport function storageGet(key: string): string | null {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n const val = api.getStorageSync(key);\n return val || null;\n } catch {\n return null;\n }\n }\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nexport function storageSet(key: string, value: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.setStorageSync(key, value);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.setItem(key, value);\n } catch {\n /* ignore */\n }\n}\n\nexport function storageRemove(key: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.removeStorageSync(key);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n}\n","/**\n * 请求签名工具\n *\n * 参考 AWS S3 / 微信支付 / 高德地图的大厂 SDK 鉴权设计:\n * - 每个请求携带 AccessKey(公开标识)+ 签名(HMAC-SHA256)\n * - 签名内容包含:HTTP方法、路径、时间戳、请求体哈希、AccessKey\n * - SecretKey 仅在本地参与签名计算,不会出现在网络请求中\n *\n * 安全特性:\n * - HMAC 签名 → 请求内容未被篡改\n * - 时间戳 → 请求超过 5 分钟无效(防重放)\n * - Nonce 随机串 → 同一时间窗口内不可重复(防重放)\n */\n\n/** 签名计算所需的凭证 */\nexport interface SignCredential {\n accessKey: string;\n secretKey: string;\n}\n\n/**\n * 计算 body 的 SHA256 哈希(hex)。\n * 小游戏环境无原生 crypto.subtle 同步 API,这里用简化实现兼容多端。\n */\nfunction sha256Hex(input: string): string {\n // 优先使用微信/抖音小游戏的 crypto(同步)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHash) {\n return g.crypto.createHash('sha256').update(input).digest('hex');\n }\n // Node 环境\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHash('sha256').update(input).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简单哈希(非加密安全,仅用于无法获取 crypto 的环境)\n let h1 = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h1 ^= input.charCodeAt(i);\n h1 = Math.imul(h1, 0x01000193);\n }\n return (h1 >>> 0).toString(16).padStart(8, '0').repeat(8);\n}\n\n/**\n * HMAC-SHA256 签名。\n * 优先使用原生 crypto,兜底用纯 JS 实现。\n */\nfunction hmacSha256(key: string, message: string): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHmac) {\n return g.crypto.createHmac('sha256', key).update(message).digest('hex');\n }\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHmac('sha256', key).update(message).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简化 HMAC(非加密安全)\n const blockLen = 64;\n const k = key.length > blockLen ? sha256Hex(key) : key.padEnd(blockLen, '\\0');\n const ipad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x36)).join('');\n const opad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x5c)).join('');\n return sha256Hex(opad + sha256Hex(ipad + message));\n}\n\n/** 生成随机 Nonce(16 位) */\nexport function generateNonce(): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.randomBytes) {\n return g.crypto.randomBytes(8).toString('hex');\n }\n let s = '';\n const chars = '0123456789abcdef';\n for (let i = 0; i < 16; i++) s += chars[Math.floor(Math.random() * 16)];\n return s;\n}\n\n/**\n * 构造请求签名头。\n *\n * 签名算法(HMAC-SHA256):\n * signStr = method + \"\\n\" + path + \"\\n\" + timestamp + \"\\n\" + sha256(body) + \"\\n\" + accessKey\n * signature = HMAC_SHA256(secretKey, signStr)\n *\n * @param method HTTP 方法\n * @param path 请求路径(不含 baseUrl,如 /game-api/xxx/score/submit)\n * @param body 请求体(对象或 undefined)\n * @param credential AK/SK 凭证\n * @returns 签名头对象,可直接合并到请求 headers\n */\nexport function buildSignHeaders(\n method: string,\n path: string,\n body: unknown,\n credential: SignCredential,\n): Record<string, string> {\n const timestamp = Math.floor(Date.now() / 1000);\n const nonce = generateNonce();\n const bodyStr = body !== undefined && body !== null ? JSON.stringify(body) : '';\n const bodyHash = sha256Hex(bodyStr);\n\n const signStr = [method, path, String(timestamp), bodyHash, credential.accessKey].join('\\n');\n const signature = hmacSha256(credential.secretKey, signStr);\n\n return {\n 'X-Access-Key': credential.accessKey,\n 'X-Timestamp': String(timestamp),\n 'X-Nonce': nonce,\n 'X-Signature': signature,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACeA,WAAS,gBAAyB;AAChC,QAAI,OAAO,OAAO,eAAe,OAAQ,GAAW,YAAY,WAAY,QAAO;AACnF,QAAI,OAAO,OAAO,eAAe,OAAQ,GAAW,YAAY,WAAY,QAAO;AACnF,WAAO;AAAA,EACT;AAEA,MAAM,UAAU,cAAc;AAgB9B,iBAAsB,QAAiB,KAA4C;AACjF,QAAI,YAAY,QAAQ,YAAY,MAAM;AACxC,aAAO,mBAAsB,GAAG;AAAA,IAClC;AACA,WAAO,aAAgB,GAAG;AAAA,EAC5B;AAGA,WAAS,mBAAsB,KAA4C;AACzE,UAAM,MAAO,YAAY,OAAO,KAAK;AACrC,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,UAAI,QAAQ;AAAA,QACV,KAAK,IAAI;AAAA,QACT,QAAQ,IAAI;AAAA,QACZ,QAAQ,IAAI,WAAW,CAAC;AAAA,QACxB,MAAM,IAAI;AAAA,QACV,SAAS,CAAC,QAAa;AACrB,kBAAQ;AAAA,YACN,QAAQ,IAAI;AAAA,YACZ,MAAM,IAAI;AAAA,UACZ,CAAC;AAAA,QACH;AAAA,QACA,MAAM,CAAC,QAAa;AAClB,iBAAO,IAAI,MAAM,KAAK,UAAU,gBAAgB,CAAC;AAAA,QACnD;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAGA,iBAAe,aAAgB,KAA4C;AACzE,UAAM,MAAM,MAAM,MAAM,IAAI,KAAK;AAAA,MAC/B,QAAQ,IAAI;AAAA,MACZ,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,IAAI,QAAQ;AAAA,MAC9D,MAAM,IAAI,SAAS,SAAY,KAAK,UAAU,IAAI,IAAI,IAAI;AAAA,IAC5D,CAAC;AACD,UAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC9C,WAAO,EAAE,QAAQ,IAAI,QAAQ,KAAgB;AAAA,EAC/C;AAIO,WAAS,WAAW,KAA4B;AACrD,QAAI,YAAY,QAAQ,YAAY,MAAM;AACxC,YAAM,MAAO,YAAY,OAAO,KAAK;AACrC,UAAI;AACF,cAAM,MAAM,IAAI,eAAe,GAAG;AAClC,eAAO,OAAO;AAAA,MAChB,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AACA,QAAI;AACF,aAAO,aAAa,QAAQ,GAAG;AAAA,IACjC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAEO,WAAS,WAAW,KAAa,OAAqB;AAC3D,QAAI,YAAY,QAAQ,YAAY,MAAM;AACxC,YAAM,MAAO,YAAY,OAAO,KAAK;AACrC,UAAI;AACF,YAAI,eAAe,KAAK,KAAK;AAAA,MAC/B,QAAQ;AAAA,MAER;AACA;AAAA,IACF;AACA,QAAI;AACF,mBAAa,QAAQ,KAAK,KAAK;AAAA,IACjC,QAAQ;AAAA,IAER;AAAA,EACF;AAEO,WAAS,cAAc,KAAmB;AAC/C,QAAI,YAAY,QAAQ,YAAY,MAAM;AACxC,YAAM,MAAO,YAAY,OAAO,KAAK;AACrC,UAAI;AACF,YAAI,kBAAkB,GAAG;AAAA,MAC3B,QAAQ;AAAA,MAER;AACA;AAAA,IACF;AACA,QAAI;AACF,mBAAa,WAAW,GAAG;AAAA,IAC7B,QAAQ;AAAA,IAER;AAAA,EACF;;;ACxGA,WAAS,UAAU,OAAuB;AAGxC,UAAM,IAAI;AACV,QAAI,EAAE,QAAQ,YAAY;AACxB,aAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAAA,IACjE;AAEA,QAAI,OAAO,cAAY,YAAY;AACjC,UAAI;AAEF,cAAM,aAAa,UAAQ,QAAQ;AACnC,eAAO,WAAW,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAAA,MACnE,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,QAAI,KAAK;AACT,aAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,YAAM,MAAM,WAAW,CAAC;AACxB,WAAK,KAAK,KAAK,IAAI,QAAU;AAAA,IAC/B;AACA,YAAQ,OAAO,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,EAAE,OAAO,CAAC;AAAA,EAC1D;AAMA,WAAS,WAAW,KAAa,SAAyB;AAExD,UAAM,IAAI;AACV,QAAI,EAAE,QAAQ,YAAY;AACxB,aAAO,EAAE,OAAO,WAAW,UAAU,GAAG,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAAA,IACxE;AACA,QAAI,OAAO,cAAY,YAAY;AACjC,UAAI;AAEF,cAAM,aAAa,UAAQ,QAAQ;AACnC,eAAO,WAAW,WAAW,UAAU,GAAG,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAAA,MAC1E,QAAQ;AAAA,MAER;AAAA,IACF;AAEA,UAAM,WAAW;AACjB,UAAM,IAAI,IAAI,SAAS,WAAW,UAAU,GAAG,IAAI,IAAI,OAAO,UAAU,IAAI;AAC5E,UAAM,OAAO,MAAM,KAAK,CAAC,EAAE,IAAI,CAAC,MAAM,OAAO,aAAa,EAAE,WAAW,CAAC,IAAI,EAAI,CAAC,EAAE,KAAK,EAAE;AAC1F,UAAM,OAAO,MAAM,KAAK,CAAC,EAAE,IAAI,CAAC,MAAM,OAAO,aAAa,EAAE,WAAW,CAAC,IAAI,EAAI,CAAC,EAAE,KAAK,EAAE;AAC1F,WAAO,UAAU,OAAO,UAAU,OAAO,OAAO,CAAC;AAAA,EACnD;AAGO,WAAS,gBAAwB;AAEtC,UAAM,IAAI;AACV,QAAI,EAAE,QAAQ,aAAa;AACzB,aAAO,EAAE,OAAO,YAAY,CAAC,EAAE,SAAS,KAAK;AAAA,IAC/C;AACA,QAAI,IAAI;AACR,UAAM,QAAQ;AACd,aAAS,IAAI,GAAG,IAAI,IAAI,IAAK,MAAK,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,EAAE,CAAC;AACtE,WAAO;AAAA,EACT;AAeO,WAAS,iBACd,QACA,MACA,MACA,YACwB;AACxB,UAAM,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC9C,UAAM,QAAQ,cAAc;AAC5B,UAAM,UAAU,SAAS,UAAa,SAAS,OAAO,KAAK,UAAU,IAAI,IAAI;AAC7E,UAAM,WAAW,UAAU,OAAO;AAElC,UAAM,UAAU,CAAC,QAAQ,MAAM,OAAO,SAAS,GAAG,UAAU,WAAW,SAAS,EAAE,KAAK,IAAI;AAC3F,UAAM,YAAY,WAAW,WAAW,WAAW,OAAO;AAE1D,WAAO;AAAA,MACL,gBAAgB,WAAW;AAAA,MAC3B,eAAe,OAAO,SAAS;AAAA,MAC/B,WAAW;AAAA,MACX,eAAe;AAAA,IACjB;AAAA,EACF;;;AF9EO,MAAM,eAAN,cAA2B,MAAM;AAAA,IACtC,YACE,SACO,QACA,MACP;AACA,YAAM,OAAO;AAHN;AACA;AAGP,WAAK,OAAO;AAAA,IACd;AAAA,EACF;AA0DO,WAAS,cAAc,QAAgC;AAG5D,UAAM,gBAAgB,qBAAqB,MAAM;AAGjD,QAAI,CAAC,cAAc,QAAQ,CAAC,cAAc,QAAQ;AAChD,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAGA,QAAI,CAAC,cAAc,SAAS,CAAC,cAAc,aAAa,CAAC,cAAc,YAAY;AACjF,YAAM,IAAI;AAAA,QACR;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,SAAS,cAAc,UACzB,GAAG,cAAc,OAAO,aAAa,cAAc,MAAM,KACzD;AACJ,UAAM,WAAW,YAAY,cAAc,UAAU,MAAM;AAG3D,QAAI,QAAuB,WAAW,QAAQ;AAE9C,aAAS,WAA0B;AACjC,aAAO;AAAA,IACT;AAEA,aAAS,SAAS,GAAwB;AACxC,cAAQ;AACR,UAAI,EAAG,YAAW,UAAU,CAAC;AAAA,UACxB,eAAc,QAAQ;AAAA,IAC7B;AAGA,mBAAe,KACb,QACA,MACA,MACY;AACZ,UAAI,cAAc,MAAM;AACtB,eAAO,aAAgB,QAAQ,MAAM,IAAI;AAAA,MAC3C;AAEA,YAAM,UAAkC,CAAC;AAGzC,UAAI,cAAc,aAAa,cAAc,WAAW;AACtD,cAAM,cAAc;AAAA,UAClB;AAAA,UACA;AAAA,UACA;AAAA,UACA,EAAE,WAAW,cAAc,WAAW,WAAW,cAAc,UAAU;AAAA,QAC3E;AACA,eAAO,OAAO,SAAS,WAAW;AAAA,MACpC;AAGA,UAAI,MAAO,SAAQ,eAAe,IAAI,UAAU,KAAK;AAErD,YAAM,MAAM,MAAM,QAAW;AAAA,QAC3B,KAAK,GAAG,MAAM,GAAG,IAAI;AAAA,QACrB;AAAA,QACA;AAAA,QACA;AAAA,MACF,CAAC;AAED,UAAI,IAAI,UAAU,KAAK;AACrB,cAAM,MAAO,IAAI,MAAc,WAAW,QAAQ,IAAI,MAAM;AAC5D,cAAM,IAAI,aAAa,KAAK,IAAI,QAAQ,IAAI,IAAI;AAAA,MAClD;AACA,aAAO,IAAI;AAAA,IACb;AAGA,UAAM,MAAe;AAAA,MACnB,MAAM;AAAA,QACJ,MAAM,MAAM,MAA0C;AACpD,gBAAM,SAAS,MAAM,KAAkB,QAAQ,eAAe,IAAI;AAClE,mBAAS,OAAO,KAAK;AACrB,iBAAO;AAAA,QACT;AAAA,QACA,IAAI,MAAM,KAAoB,OAAO,UAAU;AAAA,QAC/C,QAAQ,MAAM,SAAS,IAAI;AAAA,QAC3B;AAAA,MACF;AAAA,MAEA,OAAO;AAAA,QACL,QAAQ,CAAC,SAA6B,KAAwB,QAAQ,iBAAiB,IAAI;AAAA,MAC7F;AAAA,MAEA,aAAa;AAAA,QACX,KAAK,CAAC,UACJ,KAAyB,OAAO,sBAAsB,SAAS,GAAG,EAAE;AAAA,QACtE,QAAQ,MAAM,KAAa,OAAO,iBAAiB;AAAA,QACnD,QAAQ,CAAC,UACP,KAAyB,OAAO,6BAA6B,SAAS,CAAC,EAAE;AAAA,MAC7E;AAAA,MAEA,QAAQ;AAAA,QACN,YAAY,MAAM,KAAoB,OAAO,iBAAiB;AAAA,QAC9D,eAAe,CAAC,UAAU,KAAoB,SAAS,mBAAmB,KAAK;AAAA,MACjF;AAAA,MAEA,MAAM;AAAA,QACJ,KAAK,CAAC,KAAK,UAAU,KAAK,QAAQ,YAAY,GAAG,IAAI,EAAE,MAAM,CAAC;AAAA,QAC9D,KAAK,CAAC,QAAQ,KAAK,OAAO,YAAY,GAAG,EAAE;AAAA,QAC3C,QAAQ,MAAM,KAAK,OAAO,UAAU;AAAA,QACpC,QAAQ,CAAC,QAAQ,KAAK,UAAU,YAAY,GAAG,EAAE;AAAA,QACjD,OAAO,MAAM,KAAK,UAAU,UAAU;AAAA,MACxC;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAWA,WAAS,qBAAqB,QAAsC;AAClE,QAAI,WAAmC,CAAC;AACxC,QAAI;AAGF,iBAAY,OAAO,oBAAoB,cAAc,kBAAkB,CAAC;AAAA,IAC1E,QAAQ;AAEN,iBAAW,CAAC;AAAA,IACd;AAEA,WAAO;AAAA,MACL,QAAQ,OAAO,UAAU,SAAS;AAAA,MAClC,SAAS,OAAO,WAAW,SAAS,WAAW;AAAA,MAC/C,WAAW,OAAO,aAAa,SAAS;AAAA,MACxC,WAAW,OAAO,aAAa,SAAS;AAAA,MACxC,MAAM,OAAO;AAAA,IACf;AAAA,EACF;AAIA,MAAI,YAAY;AAChB,MAAI,YAAiD,CAAC;AACtD,MAAI,YAA2B;AAE/B,iBAAe,aAAgB,QAAgB,MAAc,MAAuB;AAClF,UAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAG1C,QAAI,SAAS,eAAe;AAC1B,kBAAY,cAAc,KAAK,IAAI,CAAC;AACpC,YAAM,UAAyB;AAAA,QAC7B,QAAQ;AAAA,QACR,UAAU,MAAM,YAAY;AAAA,QAC5B,UAAU,MAAM,YAAY;AAAA,QAC5B,WAAW;AAAA,QACX,WAAW;AAAA,QACX,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,MACrC;AACA,aAAO,EAAE,OAAO,WAAW,QAAQ,eAAe,OAAO,MAAM,QAAQ;AAAA,IACzE;AACA,QAAI,SAAS,YAAY;AACvB,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,UAAU;AAAA,QACV,WAAW;AAAA,QACX,WAAW;AAAA,QACX,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,MACrC;AAAA,IACF;AAGA,QAAI,SAAS,iBAAiB;AAC5B,YAAM,OAAO;AACb,kBAAY,KAAK,IAAI,WAAW,MAAM,SAAS,CAAC;AAChD,aAAO;AAAA,QACL,WAAW,MAAM,SAAS;AAAA,QAC1B,cAAc,MAAM,SAAS,KAAK;AAAA,QAClC,cAAc;AAAA,QACd,WAAW;AAAA,QACX,MAAM;AAAA,MACR;AAAA,IACF;AAGA,QAAI,KAAK,WAAW,eAAe,GAAG;AACpC,aAAO,CAAC;AAAA,QACN,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,OAAO;AAAA,QACP,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AACA,QAAI,SAAS,mBAAmB;AAC9B,aAAO,EAAE,MAAM,GAAG,OAAO,WAAW,cAAc,GAAG,WAAW,EAAE;AAAA,IACpE;AACA,QAAI,KAAK,WAAW,qBAAqB,GAAG;AAC1C,aAAO,CAAC;AAAA,QACN,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,OAAO;AAAA,QACP,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAGA,QAAI,SAAS,qBAAqB,WAAW,OAAO;AAClD,aAAO;AAAA,QACL,QAAQ;AAAA,QACR,UAAU;AAAA,QACV,UAAU;AAAA,QACV,WAAW;AAAA,QACX,WAAW;AAAA,QACX,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,QAClC,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,MACrC;AAAA,IACF;AAGA,QAAI,KAAK,WAAW,WAAW,KAAK,WAAW,QAAQ;AACrD,YAAM,MAAM,KAAK,MAAM,WAAW,EAAE,CAAC;AACrC,gBAAU,GAAG,IAAI,MAAM,SAAS,CAAC;AACjC,aAAO,EAAE,SAAS,MAAM,KAAK,OAAO,UAAU,GAAG,EAAE;AAAA,IACrD;AACA,QAAI,KAAK,WAAW,WAAW,KAAK,WAAW,OAAO;AACpD,YAAM,MAAM,KAAK,MAAM,WAAW,EAAE,CAAC;AACrC,aAAQ,UAAU,GAAG,KAAK;AAAA,IAC5B;AACA,QAAI,SAAS,cAAc,WAAW,OAAO;AAC3C,aAAO;AAAA,IACT;AACA,QAAI,KAAK,WAAW,WAAW,KAAK,WAAW,UAAU;AACvD,YAAM,MAAM,KAAK,MAAM,WAAW,EAAE,CAAC;AACrC,aAAO,UAAU,GAAG;AACpB,aAAO,EAAE,SAAS,MAAM,IAAI;AAAA,IAC9B;AACA,QAAI,SAAS,cAAc,WAAW,UAAU;AAC9C,YAAM,QAAQ,OAAO,KAAK,SAAS,EAAE;AACrC,kBAAY,CAAC;AACb,aAAO,EAAE,SAAS,MAAM,SAAS,MAAM;AAAA,IACzC;AAEA,WAAO,CAAC;AAAA,EACV;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/platform.ts","../src/sign.ts"],"sourcesContent":["/**\n * @zhimakechuang/game-sdk\n *\n * game-factory 平台专属游戏 SDK,封装 game-server 所有 HTTP 接口。\n *\n * 用法(模板开发 + 生产运行统一入口):\n * import { createGameSdk } from '@zhimakechuang/game-sdk';\n * const sdk = createGameSdk({\n * templateId: 'tpl_xxx', // 模板 ID\n * accessKey: 'ak_tpl_xxx', // 平台入驻后颁发的凭证\n * secretKey: 'sk_tpl_xxx', // 平台入驻后颁发的密钥\n * });\n * await sdk.auth.login({ code: 'xxx', platform: 'wx' });\n * await sdk.score.submit({ score: 1000 });\n *\n * 用法(CDN):\n * <script src=\"https://unpkg.com/@zhimakechuang/game-sdk/dist/index.global.js\"></script>\n * const sdk = GameSDK.createGameSdk({ templateId, accessKey, secretKey });\n *\n * 用法(模拟模式,模板开发调试,不连服务端):\n * const sdk = createGameSdk({ templateId, accessKey, secretKey, mock: true });\n *\n * 鉴权说明:\n * - accessKey/secretKey 必须传入,只有平台入驻开发者才能获取\n * - gameId 由平台构建时注入到 __GAME_CONFIG__,SDK 自动读取,开发者无需传入\n * - baseUrl 内置默认值,指向平台 API 服务\n */\n\nimport {\n GameSdkConfig, LoginOptions, LoginResult, PlayerProfile,\n SubmitScoreOptions, SubmitScoreResult,\n LeaderboardEntry, MyRank,\n} from './types';\nimport { request, storageGet, storageSet, storageRemove } from './platform';\nimport { buildSignHeaders } from './sign';\n\n// 运行时由平台构建时注入的配置(esbuild define)\n// gameId: 游戏实例 ID(用户创建游戏后由平台注入)\n// baseUrl: API 服务地址(平台构建时注入,开发者无需配置)\n// 凭证(accessKey/secretKey)由开发者在 createGameSdk 时传入,不通过此渠道注入\ndeclare const __GAME_CONFIG__: {\n gameId?: string;\n baseUrl?: string;\n} | undefined;\n\n// re-export 所有类型,供外部使用\nexport * from './types';\n\n// ─── 错误 ──────────────────────────────────────────────\n\nexport class GameSdkError extends Error {\n constructor(\n message: string,\n public status: number,\n public data?: any,\n ) {\n super(message);\n this.name = 'GameSdkError';\n }\n}\n\n// ─── SDK 接口定义 ───────────────────────────────────────\n\nexport interface GameSdk {\n /** 身份与登录 */\n auth: {\n /** 登录(微信/抖音/访客),成功后自动持久化 token */\n login(opts: LoginOptions): Promise<LoginResult>;\n /** 获取当前登录玩家信息(需先 login) */\n me(): Promise<PlayerProfile>;\n /** 登出,清除本地 token */\n logout(): void;\n /** 获取当前 token(用于调试) */\n getToken(): string | null;\n };\n /** 分数 */\n score: {\n /** 提交分数(需登录) */\n submit(opts: SubmitScoreOptions): Promise<SubmitScoreResult>;\n };\n /** 排行榜 */\n leaderboard: {\n /** 获取 Top N(匿名可访问,limit 默认 100) */\n top(limit?: number): Promise<LeaderboardEntry[]>;\n /** 获取当前玩家排名(需登录) */\n myRank(): Promise<MyRank>;\n /** 获取附近排名(需登录,range 默认 ±5) */\n nearby(range?: number): Promise<LeaderboardEntry[]>;\n };\n /** 玩家档案 */\n player: {\n /** 获取当前玩家档案(需登录) */\n getProfile(): Promise<PlayerProfile>;\n /** 更新档案(昵称/头像/扩展字段) */\n updateProfile(patch: {\n nickname?: string;\n avatarUrl?: string;\n extra?: Record<string, any>;\n }): Promise<PlayerProfile>;\n };\n /** 云存档(KV) */\n save: {\n /** 保存数据到指定 key */\n set(key: string, value: Record<string, any>): Promise<{ success: boolean; key: string; value: Record<string, any> }>;\n /** 读取指定 key 的数据 */\n get(key: string): Promise<Record<string, any> | null>;\n /** 读取所有存档 */\n getAll(): Promise<Record<string, any>>;\n /** 删除指定 key */\n remove(key: string): Promise<{ success: boolean; key: string }>;\n /** 清空所有存档 */\n clear(): Promise<{ success: boolean; deleted: number }>;\n };\n}\n\n// ─── 创建 SDK ───────────────────────────────────────────\n\nexport function createGameSdk(config: GameSdkConfig): GameSdk {\n // 凭证是必填项,从源头阻止未授权调用\n if (!config.templateId) {\n throw new GameSdkError(\n '缺少 templateId:请在平台创建模板后获取模板 ID',\n 400,\n );\n }\n if (!config.accessKey || !config.secretKey) {\n throw new GameSdkError(\n '缺少 accessKey/secretKey:请确认已在平台入驻并获取开发者凭证',\n 400,\n );\n }\n\n // gameId 和 baseUrl 由平台构建时注入到 __GAME_CONFIG__,运行时自动读取\n // 模板开发阶段(mock 模式)不需要 gameId\n const { gameId, baseUrl } = resolveInjectedConfig();\n\n if (!config.mock && !gameId) {\n throw new GameSdkError(\n '缺少 gameId:请确认游戏已通过平台构建,或使用 mock 模式进行模板开发',\n 400,\n );\n }\n\n // baseUrl 由平台注入,开发者无需配置\n const apiUrl = gameId\n ? `${baseUrl}/game-api/${gameId}`\n : '';\n const tokenKey = `gf_token_${gameId ?? 'mock'}`;\n\n // token 管理(从本地存储恢复)\n let token: string | null = storageGet(tokenKey);\n\n function getToken(): string | null {\n return token;\n }\n\n function setToken(t: string | null): void {\n token = t;\n if (t) storageSet(tokenKey, t);\n else storageRemove(tokenKey);\n }\n\n // ── 内部 HTTP 封装 ──────────────────────────────────\n async function call<T = any>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n body?: any,\n ): Promise<T> {\n if (config.mock) {\n return mockResponse<T>(method, path, body);\n }\n\n const headers: Record<string, string> = {};\n\n // 附加请求签名(HMAC-SHA256,防篡改 + 防重放)\n const signHeaders = buildSignHeaders(\n method,\n path,\n body,\n { accessKey: config.accessKey, secretKey: config.secretKey },\n );\n Object.assign(headers, signHeaders);\n\n // 附加玩家 token\n if (token) headers['Authorization'] = `Bearer ${token}`;\n\n const res = await request<T>({\n url: `${apiUrl}${path}`,\n method,\n headers,\n body,\n });\n\n if (res.status >= 400) {\n const msg = (res.data as any)?.message || `HTTP ${res.status}`;\n throw new GameSdkError(msg, res.status, res.data);\n }\n return res.data;\n }\n\n // ── 方法面 ──────────────────────────────────────────\n const sdk: GameSdk = {\n auth: {\n async login(opts: LoginOptions): Promise<LoginResult> {\n const result = await call<LoginResult>('POST', '/auth/login', opts);\n setToken(result.token);\n return result;\n },\n me: () => call<PlayerProfile>('GET', '/auth/me'),\n logout: () => setToken(null),\n getToken,\n },\n\n score: {\n submit: (opts: SubmitScoreOptions) => call<SubmitScoreResult>('POST', '/score/submit', opts),\n },\n\n leaderboard: {\n top: (limit?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard?limit=${limit || 100}`),\n myRank: () => call<MyRank>('GET', '/leaderboard/me'),\n nearby: (range?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard/nearby?range=${range || 5}`),\n },\n\n player: {\n getProfile: () => call<PlayerProfile>('GET', '/player/profile'),\n updateProfile: (patch) => call<PlayerProfile>('PATCH', '/player/profile', patch),\n },\n\n save: {\n set: (key, value) => call('POST', `/storage/${key}`, { value }),\n get: (key) => call('GET', `/storage/${key}`),\n getAll: () => call('GET', '/storage'),\n remove: (key) => call('DELETE', `/storage/${key}`),\n clear: () => call('DELETE', '/storage'),\n },\n };\n\n return sdk;\n}\n\n// ─── 运行时注入配置解析 ─────────────────────────────────\n\n/**\n * 从 __GAME_CONFIG__ 读取平台构建时注入的 gameId 和 baseUrl。\n *\n * gameId: 游戏实例 ID,只有用户通过平台创建游戏并构建后才会存在。\n * 模板开发阶段(mock 模式)不需要 gameId。\n * baseUrl: API 服务地址,由平台构建时注入,开发者无需手动配置。\n *\n * __GAME_CONFIG__ 由平台构建时通过 esbuild define 注入:\n * define: { '__GAME_CONFIG__': JSON.stringify({ gameId, baseUrl }) }\n */\nfunction resolveInjectedConfig(): { gameId?: string; baseUrl: string } {\n try {\n // eslint-disable-next-line no-undef\n const injected = (typeof __GAME_CONFIG__ !== 'undefined' ? __GAME_CONFIG__ : {}) as {\n gameId?: string;\n baseUrl?: string;\n };\n return {\n gameId: injected.gameId,\n baseUrl: injected.baseUrl ?? '',\n };\n } catch {\n return { gameId: undefined, baseUrl: '' };\n }\n}\n\n// ─── Mock 模式(模板开发不连服务端)──────────────────────\n\nlet mockScore = 0;\nlet mockSaves: Record<string, Record<string, any>> = {};\nlet mockToken: string | null = null;\n\nasync function mockResponse<T>(method: string, path: string, body: any): Promise<T> {\n await new Promise((r) => setTimeout(r, 50)); // 模拟网络延迟\n\n // auth\n if (path === '/auth/login') {\n mockToken = `mock_token_${Date.now()}`;\n const profile: PlayerProfile = {\n openId: 'mock_openid',\n platform: body?.platform || 'guest',\n nickname: body?.nickname || '测试玩家',\n bestScore: mockScore,\n playCount: 0,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n };\n return { token: mockToken, openId: 'mock_openid', isNew: true, profile } as T;\n }\n if (path === '/auth/me') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // score\n if (path === '/score/submit') {\n const prev = mockScore;\n mockScore = Math.max(mockScore, body?.score || 0);\n return {\n submitted: body?.score || 0,\n isHighScore: (body?.score || 0) > prev,\n previousBest: prev,\n bestScore: mockScore,\n rank: 1,\n } as T;\n }\n\n // leaderboard\n if (path.startsWith('/leaderboard?')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n if (path === '/leaderboard/me') {\n return { rank: 1, score: mockScore, totalPlayers: 1, playCount: 1 } as T;\n }\n if (path.startsWith('/leaderboard/nearby')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n\n // player\n if (path === '/player/profile' && method === 'GET') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // storage\n if (path.startsWith('/storage/') && method === 'POST') {\n const key = path.split('/storage/')[1];\n mockSaves[key] = body?.value || {};\n return { success: true, key, value: mockSaves[key] } as T;\n }\n if (path.startsWith('/storage/') && method === 'GET') {\n const key = path.split('/storage/')[1];\n return (mockSaves[key] || null) as T;\n }\n if (path === '/storage' && method === 'GET') {\n return mockSaves as T;\n }\n if (path.startsWith('/storage/') && method === 'DELETE') {\n const key = path.split('/storage/')[1];\n delete mockSaves[key];\n return { success: true, key } as T;\n }\n if (path === '/storage' && method === 'DELETE') {\n const count = Object.keys(mockSaves).length;\n mockSaves = {};\n return { success: true, deleted: count } as T;\n }\n\n return {} as T;\n}\n","/**\n * 平台适配层\n *\n * 自动检测运行环境(微信小游戏 / 抖音小游戏 / H5),\n * 封装 HTTP 请求和本地存储,对上层提供统一接口。\n */\n\n// 小游戏运行时的全局对象(微信/抖音),H5 下不存在\ndeclare const wx: any;\ndeclare const tt: any;\n\n// ─── 环境检测 ──────────────────────────────────────────\n\ntype Runtime = 'wx' | 'tt' | 'h5';\n\nfunction detectRuntime(): Runtime {\n if (typeof wx !== 'undefined' && typeof (wx as any).request === 'function') return 'wx';\n if (typeof tt !== 'undefined' && typeof (tt as any).request === 'function') return 'tt';\n return 'h5';\n}\n\nconst runtime = detectRuntime();\n\n// ─── HTTP 请求 ─────────────────────────────────────────\n\nexport interface HttpRequest {\n url: string;\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n headers?: Record<string, string>;\n body?: any;\n}\n\nexport interface HttpResponse<T = any> {\n status: number;\n data: T;\n}\n\nexport async function request<T = any>(req: HttpRequest): Promise<HttpResponse<T>> {\n if (runtime === 'wx' || runtime === 'tt') {\n return miniProgramRequest<T>(req);\n }\n return fetchRequest<T>(req);\n}\n\n/** 微信/抖音小游戏请求(wx.request / tt.request) */\nfunction miniProgramRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const api = (runtime === 'wx' ? wx : tt) as any;\n return new Promise((resolve, reject) => {\n api.request({\n url: req.url,\n method: req.method,\n header: req.headers || {},\n data: req.body,\n success: (res: any) => {\n resolve({\n status: res.statusCode,\n data: res.data as T,\n });\n },\n fail: (err: any) => {\n reject(new Error(err?.errMsg || 'request failed'));\n },\n });\n });\n}\n\n/** H5 fetch 请求 */\nasync function fetchRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const res = await fetch(req.url, {\n method: req.method,\n headers: { 'Content-Type': 'application/json', ...req.headers },\n body: req.body !== undefined ? JSON.stringify(req.body) : undefined,\n });\n const data = await res.json().catch(() => ({}));\n return { status: res.status, data: data as T };\n}\n\n// ─── 本地存储(token 持久化)────────────────────────────\n\nexport function storageGet(key: string): string | null {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n const val = api.getStorageSync(key);\n return val || null;\n } catch {\n return null;\n }\n }\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nexport function storageSet(key: string, value: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.setStorageSync(key, value);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.setItem(key, value);\n } catch {\n /* ignore */\n }\n}\n\nexport function storageRemove(key: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.removeStorageSync(key);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n}\n","/**\n * 请求签名工具\n *\n * 参考 AWS S3 / 微信支付 / 高德地图的大厂 SDK 鉴权设计:\n * - 每个请求携带 AccessKey(公开标识)+ 签名(HMAC-SHA256)\n * - 签名内容包含:HTTP方法、路径、时间戳、请求体哈希、AccessKey\n * - SecretKey 仅在本地参与签名计算,不会出现在网络请求中\n *\n * 安全特性:\n * - HMAC 签名 → 请求内容未被篡改\n * - 时间戳 → 请求超过 5 分钟无效(防重放)\n * - Nonce 随机串 → 同一时间窗口内不可重复(防重放)\n */\n\n/** 签名计算所需的凭证 */\nexport interface SignCredential {\n accessKey: string;\n secretKey: string;\n}\n\n/**\n * 计算 body 的 SHA256 哈希(hex)。\n * 小游戏环境无原生 crypto.subtle 同步 API,这里用简化实现兼容多端。\n */\nfunction sha256Hex(input: string): string {\n // 优先使用微信/抖音小游戏的 crypto(同步)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHash) {\n return g.crypto.createHash('sha256').update(input).digest('hex');\n }\n // Node 环境\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHash('sha256').update(input).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简单哈希(非加密安全,仅用于无法获取 crypto 的环境)\n let h1 = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h1 ^= input.charCodeAt(i);\n h1 = Math.imul(h1, 0x01000193);\n }\n return (h1 >>> 0).toString(16).padStart(8, '0').repeat(8);\n}\n\n/**\n * HMAC-SHA256 签名。\n * 优先使用原生 crypto,兜底用纯 JS 实现。\n */\nfunction hmacSha256(key: string, message: string): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHmac) {\n return g.crypto.createHmac('sha256', key).update(message).digest('hex');\n }\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHmac('sha256', key).update(message).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简化 HMAC(非加密安全)\n const blockLen = 64;\n const k = key.length > blockLen ? sha256Hex(key) : key.padEnd(blockLen, '\\0');\n const ipad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x36)).join('');\n const opad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x5c)).join('');\n return sha256Hex(opad + sha256Hex(ipad + message));\n}\n\n/** 生成随机 Nonce(16 位) */\nexport function generateNonce(): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.randomBytes) {\n return g.crypto.randomBytes(8).toString('hex');\n }\n let s = '';\n const chars = '0123456789abcdef';\n for (let i = 0; i < 16; i++) s += chars[Math.floor(Math.random() * 16)];\n return s;\n}\n\n/**\n * 构造请求签名头。\n *\n * 签名算法(HMAC-SHA256):\n * signStr = method + \"\\n\" + path + \"\\n\" + timestamp + \"\\n\" + sha256(body) + \"\\n\" + accessKey\n * signature = HMAC_SHA256(secretKey, signStr)\n *\n * @param method HTTP 方法\n * @param path 请求路径(不含 baseUrl,如 /game-api/xxx/score/submit)\n * @param body 请求体(对象或 undefined)\n * @param credential AK/SK 凭证\n * @returns 签名头对象,可直接合并到请求 headers\n */\nexport function buildSignHeaders(\n method: string,\n path: string,\n body: unknown,\n credential: SignCredential,\n): Record<string, string> {\n const timestamp = Math.floor(Date.now() / 1000);\n const nonce = generateNonce();\n const bodyStr = body !== undefined && body !== null ? JSON.stringify(body) : '';\n const bodyHash = sha256Hex(bodyStr);\n\n const signStr = [method, path, String(timestamp), bodyHash, credential.accessKey].join('\\n');\n const signature = hmacSha256(credential.secretKey, signStr);\n\n return {\n 'X-Access-Key': credential.accessKey,\n 'X-Timestamp': String(timestamp),\n 'X-Nonce': nonce,\n 'X-Signature': signature,\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -185,21 +185,27 @@ var GameSdkError = class extends Error {
|
|
|
185
185
|
}
|
|
186
186
|
};
|
|
187
187
|
function createGameSdk(config) {
|
|
188
|
-
|
|
189
|
-
if (!runtimeConfig.mock && !runtimeConfig.gameId) {
|
|
188
|
+
if (!config.templateId) {
|
|
190
189
|
throw new GameSdkError(
|
|
191
|
-
"\u7F3A\u5C11
|
|
190
|
+
"\u7F3A\u5C11 templateId\uFF1A\u8BF7\u5728\u5E73\u53F0\u521B\u5EFA\u6A21\u677F\u540E\u83B7\u53D6\u6A21\u677F ID",
|
|
192
191
|
400
|
|
193
192
|
);
|
|
194
193
|
}
|
|
195
|
-
if (!
|
|
194
|
+
if (!config.accessKey || !config.secretKey) {
|
|
196
195
|
throw new GameSdkError(
|
|
197
|
-
"\u7F3A\u5C11 accessKey/secretKey\uFF1A\u8BF7\u786E\u8BA4\
|
|
196
|
+
"\u7F3A\u5C11 accessKey/secretKey\uFF1A\u8BF7\u786E\u8BA4\u5DF2\u5728\u5E73\u53F0\u5165\u9A7B\u5E76\u83B7\u53D6\u5F00\u53D1\u8005\u51ED\u8BC1",
|
|
198
197
|
400
|
|
199
198
|
);
|
|
200
199
|
}
|
|
201
|
-
const
|
|
202
|
-
|
|
200
|
+
const { gameId, baseUrl } = resolveInjectedConfig();
|
|
201
|
+
if (!config.mock && !gameId) {
|
|
202
|
+
throw new GameSdkError(
|
|
203
|
+
"\u7F3A\u5C11 gameId\uFF1A\u8BF7\u786E\u8BA4\u6E38\u620F\u5DF2\u901A\u8FC7\u5E73\u53F0\u6784\u5EFA\uFF0C\u6216\u4F7F\u7528 mock \u6A21\u5F0F\u8FDB\u884C\u6A21\u677F\u5F00\u53D1",
|
|
204
|
+
400
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
const apiUrl = gameId ? `${baseUrl}/game-api/${gameId}` : "";
|
|
208
|
+
const tokenKey = `gf_token_${gameId ?? "mock"}`;
|
|
203
209
|
let token = storageGet(tokenKey);
|
|
204
210
|
function getToken() {
|
|
205
211
|
return token;
|
|
@@ -210,19 +216,17 @@ function createGameSdk(config) {
|
|
|
210
216
|
else storageRemove(tokenKey);
|
|
211
217
|
}
|
|
212
218
|
async function call(method, path, body) {
|
|
213
|
-
if (
|
|
219
|
+
if (config.mock) {
|
|
214
220
|
return mockResponse(method, path, body);
|
|
215
221
|
}
|
|
216
222
|
const headers = {};
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
Object.assign(headers, signHeaders);
|
|
225
|
-
}
|
|
223
|
+
const signHeaders = buildSignHeaders(
|
|
224
|
+
method,
|
|
225
|
+
path,
|
|
226
|
+
body,
|
|
227
|
+
{ accessKey: config.accessKey, secretKey: config.secretKey }
|
|
228
|
+
);
|
|
229
|
+
Object.assign(headers, signHeaders);
|
|
226
230
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
227
231
|
const res = await request({
|
|
228
232
|
url: `${apiUrl}${path}`,
|
|
@@ -269,20 +273,16 @@ function createGameSdk(config) {
|
|
|
269
273
|
};
|
|
270
274
|
return sdk;
|
|
271
275
|
}
|
|
272
|
-
function
|
|
273
|
-
let injected = {};
|
|
276
|
+
function resolveInjectedConfig() {
|
|
274
277
|
try {
|
|
275
|
-
injected = typeof __GAME_CONFIG__ !== "undefined" ? __GAME_CONFIG__ : {};
|
|
278
|
+
const injected = typeof __GAME_CONFIG__ !== "undefined" ? __GAME_CONFIG__ : {};
|
|
279
|
+
return {
|
|
280
|
+
gameId: injected.gameId,
|
|
281
|
+
baseUrl: injected.baseUrl ?? ""
|
|
282
|
+
};
|
|
276
283
|
} catch {
|
|
277
|
-
|
|
284
|
+
return { gameId: void 0, baseUrl: "" };
|
|
278
285
|
}
|
|
279
|
-
return {
|
|
280
|
-
gameId: config.gameId ?? injected.gameId,
|
|
281
|
-
baseUrl: config.baseUrl ?? injected.baseUrl ?? "",
|
|
282
|
-
accessKey: config.accessKey ?? injected.accessKey,
|
|
283
|
-
secretKey: config.secretKey ?? injected.secretKey,
|
|
284
|
-
mock: config.mock
|
|
285
|
-
};
|
|
286
286
|
}
|
|
287
287
|
var mockScore = 0;
|
|
288
288
|
var mockSaves = {};
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/platform.ts","../src/sign.ts"],"sourcesContent":["/**\n * @game-factory/game-sdk\n *\n * 通用游戏 SDK,封装 game-server-shared 所有 HTTP 接口。\n *\n * 用法(工程化):\n * import { createGameSdk } from '@game-factory/game-sdk';\n * const sdk = createGameSdk({ baseUrl: 'https://api.example.com' });\n * await sdk.auth.login({ code: 'xxx', platform: 'wx' });\n * await sdk.score.submit({ score: 1000 });\n *\n * 用法(CDN):\n * <script src=\"https://cdn.example.com/game-sdk/index.umd.js\"></script>\n * const sdk = GameSDK.createGameSdk({ baseUrl: '...' });\n *\n * 用法(模拟模式,不连服务端):\n * const sdk = createGameSdk({ baseUrl: '', mock: true });\n *\n * 凭证说明:\n * gameId / accessKey / secretKey 由平台在构建时注入到 __GAME_CONFIG__,\n * SDK 自动读取,开发者无需手动传入。\n * 模板开发阶段使用 mock 模式,不需要凭证。\n */\n\nimport {\n GameSdkConfig, LoginOptions, LoginResult, PlayerProfile,\n SubmitScoreOptions, SubmitScoreResult,\n LeaderboardEntry, MyRank,\n} from './types';\nimport { request, storageGet, storageSet, storageRemove } from './platform';\nimport { buildSignHeaders } from './sign';\n\n// 运行时由平台构建时注入的配置(esbuild define)\ndeclare const __GAME_CONFIG__: {\n gameId?: string;\n accessKey?: string;\n secretKey?: string;\n baseUrl?: string;\n} | undefined;\n\n// re-export 所有类型,供外部使用\nexport * from './types';\n\n// ─── 错误 ──────────────────────────────────────────────\n\nexport class GameSdkError extends Error {\n constructor(\n message: string,\n public status: number,\n public data?: any,\n ) {\n super(message);\n this.name = 'GameSdkError';\n }\n}\n\n// ─── SDK 接口定义 ───────────────────────────────────────\n\nexport interface GameSdk {\n /** 身份与登录 */\n auth: {\n /** 登录(微信/抖音/访客),成功后自动持久化 token */\n login(opts: LoginOptions): Promise<LoginResult>;\n /** 获取当前登录玩家信息(需先 login) */\n me(): Promise<PlayerProfile>;\n /** 登出,清除本地 token */\n logout(): void;\n /** 获取当前 token(用于调试) */\n getToken(): string | null;\n };\n /** 分数 */\n score: {\n /** 提交分数(需登录) */\n submit(opts: SubmitScoreOptions): Promise<SubmitScoreResult>;\n };\n /** 排行榜 */\n leaderboard: {\n /** 获取 Top N(匿名可访问,limit 默认 100) */\n top(limit?: number): Promise<LeaderboardEntry[]>;\n /** 获取当前玩家排名(需登录) */\n myRank(): Promise<MyRank>;\n /** 获取附近排名(需登录,range 默认 ±5) */\n nearby(range?: number): Promise<LeaderboardEntry[]>;\n };\n /** 玩家档案 */\n player: {\n /** 获取当前玩家档案(需登录) */\n getProfile(): Promise<PlayerProfile>;\n /** 更新档案(昵称/头像/扩展字段) */\n updateProfile(patch: {\n nickname?: string;\n avatarUrl?: string;\n extra?: Record<string, any>;\n }): Promise<PlayerProfile>;\n };\n /** 云存档(KV) */\n save: {\n /** 保存数据到指定 key */\n set(key: string, value: Record<string, any>): Promise<{ success: boolean; key: string; value: Record<string, any> }>;\n /** 读取指定 key 的数据 */\n get(key: string): Promise<Record<string, any> | null>;\n /** 读取所有存档 */\n getAll(): Promise<Record<string, any>>;\n /** 删除指定 key */\n remove(key: string): Promise<{ success: boolean; key: string }>;\n /** 清空所有存档 */\n clear(): Promise<{ success: boolean; deleted: number }>;\n };\n}\n\n// ─── 创建 SDK ───────────────────────────────────────────\n\nexport function createGameSdk(config: GameSdkConfig): GameSdk {\n // 合并 __GAME_CONFIG__ 注入的凭证(构建时注入,运行时自动读取)\n // 优先级:显式传入 > __GAME_CONFIG__ 注入\n const runtimeConfig = resolveRuntimeConfig(config);\n\n // 非 mock 模式必须有 gameId(游戏运行阶段由平台注入)\n if (!runtimeConfig.mock && !runtimeConfig.gameId) {\n throw new GameSdkError(\n '缺少 gameId:请确认游戏已通过平台构建,或使用 mock 模式进行模板开发',\n 400,\n );\n }\n\n // 非 mock 模式必须有凭证(游戏运行阶段由平台注入)\n if (!runtimeConfig.mock && (!runtimeConfig.accessKey || !runtimeConfig.secretKey)) {\n throw new GameSdkError(\n '缺少 accessKey/secretKey:请确认游戏已通过平台构建并颁发了模板级凭证',\n 400,\n );\n }\n\n const apiUrl = runtimeConfig.baseUrl\n ? `${runtimeConfig.baseUrl}/game-api/${runtimeConfig.gameId}`\n : '';\n const tokenKey = `gf_token_${runtimeConfig.gameId ?? 'mock'}`;\n\n // token 管理(从本地存储恢复)\n let token: string | null = storageGet(tokenKey);\n\n function getToken(): string | null {\n return token;\n }\n\n function setToken(t: string | null): void {\n token = t;\n if (t) storageSet(tokenKey, t);\n else storageRemove(tokenKey);\n }\n\n // ── 内部 HTTP 封装 ──────────────────────────────────\n async function call<T = any>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n body?: any,\n ): Promise<T> {\n if (runtimeConfig.mock) {\n return mockResponse<T>(method, path, body);\n }\n\n const headers: Record<string, string> = {};\n\n // 附加请求签名(HMAC-SHA256,防篡改 + 防重放)\n if (runtimeConfig.accessKey && runtimeConfig.secretKey) {\n const signHeaders = buildSignHeaders(\n method,\n path,\n body,\n { accessKey: runtimeConfig.accessKey, secretKey: runtimeConfig.secretKey },\n );\n Object.assign(headers, signHeaders);\n }\n\n // 附加玩家 token\n if (token) headers['Authorization'] = `Bearer ${token}`;\n\n const res = await request<T>({\n url: `${apiUrl}${path}`,\n method,\n headers,\n body,\n });\n\n if (res.status >= 400) {\n const msg = (res.data as any)?.message || `HTTP ${res.status}`;\n throw new GameSdkError(msg, res.status, res.data);\n }\n return res.data;\n }\n\n // ── 方法面 ──────────────────────────────────────────\n const sdk: GameSdk = {\n auth: {\n async login(opts: LoginOptions): Promise<LoginResult> {\n const result = await call<LoginResult>('POST', '/auth/login', opts);\n setToken(result.token);\n return result;\n },\n me: () => call<PlayerProfile>('GET', '/auth/me'),\n logout: () => setToken(null),\n getToken,\n },\n\n score: {\n submit: (opts: SubmitScoreOptions) => call<SubmitScoreResult>('POST', '/score/submit', opts),\n },\n\n leaderboard: {\n top: (limit?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard?limit=${limit || 100}`),\n myRank: () => call<MyRank>('GET', '/leaderboard/me'),\n nearby: (range?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard/nearby?range=${range || 5}`),\n },\n\n player: {\n getProfile: () => call<PlayerProfile>('GET', '/player/profile'),\n updateProfile: (patch) => call<PlayerProfile>('PATCH', '/player/profile', patch),\n },\n\n save: {\n set: (key, value) => call('POST', `/storage/${key}`, { value }),\n get: (key) => call('GET', `/storage/${key}`),\n getAll: () => call('GET', '/storage'),\n remove: (key) => call('DELETE', `/storage/${key}`),\n clear: () => call('DELETE', '/storage'),\n },\n };\n\n return sdk;\n}\n\n// ─── 运行时配置解析 ─────────────────────────────────────\n\n/**\n * 合并用户显式传入的配置与 __GAME_CONFIG__ 注入的配置。\n * 优先级:显式传入 > __GAME_CONFIG__ 注入\n *\n * __GAME_CONFIG__ 由平台构建时通过 esbuild define 注入:\n * define: { '__GAME_CONFIG__': JSON.stringify({ gameId, accessKey, secretKey, baseUrl }) }\n */\nfunction resolveRuntimeConfig(config: GameSdkConfig): GameSdkConfig {\n let injected: Partial<GameSdkConfig> = {};\n try {\n // __GAME_CONFIG__ 可能不存在(模板开发阶段 / CDN 直连)\n // eslint-disable-next-line no-undef\n injected = (typeof __GAME_CONFIG__ !== 'undefined' ? __GAME_CONFIG__ : {}) as any;\n } catch {\n // __GAME_CONFIG__ 未定义时 ReferenceError,忽略\n injected = {};\n }\n\n return {\n gameId: config.gameId ?? injected.gameId,\n baseUrl: config.baseUrl ?? injected.baseUrl ?? '',\n accessKey: config.accessKey ?? injected.accessKey,\n secretKey: config.secretKey ?? injected.secretKey,\n mock: config.mock,\n };\n}\n\n// ─── Mock 模式(模板开发不连服务端)──────────────────────\n\nlet mockScore = 0;\nlet mockSaves: Record<string, Record<string, any>> = {};\nlet mockToken: string | null = null;\n\nasync function mockResponse<T>(method: string, path: string, body: any): Promise<T> {\n await new Promise((r) => setTimeout(r, 50)); // 模拟网络延迟\n\n // auth\n if (path === '/auth/login') {\n mockToken = `mock_token_${Date.now()}`;\n const profile: PlayerProfile = {\n openId: 'mock_openid',\n platform: body?.platform || 'guest',\n nickname: body?.nickname || '测试玩家',\n bestScore: mockScore,\n playCount: 0,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n };\n return { token: mockToken, openId: 'mock_openid', isNew: true, profile } as T;\n }\n if (path === '/auth/me') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // score\n if (path === '/score/submit') {\n const prev = mockScore;\n mockScore = Math.max(mockScore, body?.score || 0);\n return {\n submitted: body?.score || 0,\n isHighScore: (body?.score || 0) > prev,\n previousBest: prev,\n bestScore: mockScore,\n rank: 1,\n } as T;\n }\n\n // leaderboard\n if (path.startsWith('/leaderboard?')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n if (path === '/leaderboard/me') {\n return { rank: 1, score: mockScore, totalPlayers: 1, playCount: 1 } as T;\n }\n if (path.startsWith('/leaderboard/nearby')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n\n // player\n if (path === '/player/profile' && method === 'GET') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // storage\n if (path.startsWith('/storage/') && method === 'POST') {\n const key = path.split('/storage/')[1];\n mockSaves[key] = body?.value || {};\n return { success: true, key, value: mockSaves[key] } as T;\n }\n if (path.startsWith('/storage/') && method === 'GET') {\n const key = path.split('/storage/')[1];\n return (mockSaves[key] || null) as T;\n }\n if (path === '/storage' && method === 'GET') {\n return mockSaves as T;\n }\n if (path.startsWith('/storage/') && method === 'DELETE') {\n const key = path.split('/storage/')[1];\n delete mockSaves[key];\n return { success: true, key } as T;\n }\n if (path === '/storage' && method === 'DELETE') {\n const count = Object.keys(mockSaves).length;\n mockSaves = {};\n return { success: true, deleted: count } as T;\n }\n\n return {} as T;\n}\n","/**\n * 平台适配层\n *\n * 自动检测运行环境(微信小游戏 / 抖音小游戏 / H5),\n * 封装 HTTP 请求和本地存储,对上层提供统一接口。\n */\n\n// 小游戏运行时的全局对象(微信/抖音),H5 下不存在\ndeclare const wx: any;\ndeclare const tt: any;\n\n// ─── 环境检测 ──────────────────────────────────────────\n\ntype Runtime = 'wx' | 'tt' | 'h5';\n\nfunction detectRuntime(): Runtime {\n if (typeof wx !== 'undefined' && typeof (wx as any).request === 'function') return 'wx';\n if (typeof tt !== 'undefined' && typeof (tt as any).request === 'function') return 'tt';\n return 'h5';\n}\n\nconst runtime = detectRuntime();\n\n// ─── HTTP 请求 ─────────────────────────────────────────\n\nexport interface HttpRequest {\n url: string;\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n headers?: Record<string, string>;\n body?: any;\n}\n\nexport interface HttpResponse<T = any> {\n status: number;\n data: T;\n}\n\nexport async function request<T = any>(req: HttpRequest): Promise<HttpResponse<T>> {\n if (runtime === 'wx' || runtime === 'tt') {\n return miniProgramRequest<T>(req);\n }\n return fetchRequest<T>(req);\n}\n\n/** 微信/抖音小游戏请求(wx.request / tt.request) */\nfunction miniProgramRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const api = (runtime === 'wx' ? wx : tt) as any;\n return new Promise((resolve, reject) => {\n api.request({\n url: req.url,\n method: req.method,\n header: req.headers || {},\n data: req.body,\n success: (res: any) => {\n resolve({\n status: res.statusCode,\n data: res.data as T,\n });\n },\n fail: (err: any) => {\n reject(new Error(err?.errMsg || 'request failed'));\n },\n });\n });\n}\n\n/** H5 fetch 请求 */\nasync function fetchRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const res = await fetch(req.url, {\n method: req.method,\n headers: { 'Content-Type': 'application/json', ...req.headers },\n body: req.body !== undefined ? JSON.stringify(req.body) : undefined,\n });\n const data = await res.json().catch(() => ({}));\n return { status: res.status, data: data as T };\n}\n\n// ─── 本地存储(token 持久化)────────────────────────────\n\nexport function storageGet(key: string): string | null {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n const val = api.getStorageSync(key);\n return val || null;\n } catch {\n return null;\n }\n }\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nexport function storageSet(key: string, value: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.setStorageSync(key, value);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.setItem(key, value);\n } catch {\n /* ignore */\n }\n}\n\nexport function storageRemove(key: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.removeStorageSync(key);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n}\n","/**\n * 请求签名工具\n *\n * 参考 AWS S3 / 微信支付 / 高德地图的大厂 SDK 鉴权设计:\n * - 每个请求携带 AccessKey(公开标识)+ 签名(HMAC-SHA256)\n * - 签名内容包含:HTTP方法、路径、时间戳、请求体哈希、AccessKey\n * - SecretKey 仅在本地参与签名计算,不会出现在网络请求中\n *\n * 安全特性:\n * - HMAC 签名 → 请求内容未被篡改\n * - 时间戳 → 请求超过 5 分钟无效(防重放)\n * - Nonce 随机串 → 同一时间窗口内不可重复(防重放)\n */\n\n/** 签名计算所需的凭证 */\nexport interface SignCredential {\n accessKey: string;\n secretKey: string;\n}\n\n/**\n * 计算 body 的 SHA256 哈希(hex)。\n * 小游戏环境无原生 crypto.subtle 同步 API,这里用简化实现兼容多端。\n */\nfunction sha256Hex(input: string): string {\n // 优先使用微信/抖音小游戏的 crypto(同步)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHash) {\n return g.crypto.createHash('sha256').update(input).digest('hex');\n }\n // Node 环境\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHash('sha256').update(input).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简单哈希(非加密安全,仅用于无法获取 crypto 的环境)\n let h1 = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h1 ^= input.charCodeAt(i);\n h1 = Math.imul(h1, 0x01000193);\n }\n return (h1 >>> 0).toString(16).padStart(8, '0').repeat(8);\n}\n\n/**\n * HMAC-SHA256 签名。\n * 优先使用原生 crypto,兜底用纯 JS 实现。\n */\nfunction hmacSha256(key: string, message: string): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHmac) {\n return g.crypto.createHmac('sha256', key).update(message).digest('hex');\n }\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHmac('sha256', key).update(message).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简化 HMAC(非加密安全)\n const blockLen = 64;\n const k = key.length > blockLen ? sha256Hex(key) : key.padEnd(blockLen, '\\0');\n const ipad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x36)).join('');\n const opad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x5c)).join('');\n return sha256Hex(opad + sha256Hex(ipad + message));\n}\n\n/** 生成随机 Nonce(16 位) */\nexport function generateNonce(): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.randomBytes) {\n return g.crypto.randomBytes(8).toString('hex');\n }\n let s = '';\n const chars = '0123456789abcdef';\n for (let i = 0; i < 16; i++) s += chars[Math.floor(Math.random() * 16)];\n return s;\n}\n\n/**\n * 构造请求签名头。\n *\n * 签名算法(HMAC-SHA256):\n * signStr = method + \"\\n\" + path + \"\\n\" + timestamp + \"\\n\" + sha256(body) + \"\\n\" + accessKey\n * signature = HMAC_SHA256(secretKey, signStr)\n *\n * @param method HTTP 方法\n * @param path 请求路径(不含 baseUrl,如 /game-api/xxx/score/submit)\n * @param body 请求体(对象或 undefined)\n * @param credential AK/SK 凭证\n * @returns 签名头对象,可直接合并到请求 headers\n */\nexport function buildSignHeaders(\n method: string,\n path: string,\n body: unknown,\n credential: SignCredential,\n): Record<string, string> {\n const timestamp = Math.floor(Date.now() / 1000);\n const nonce = generateNonce();\n const bodyStr = body !== undefined && body !== null ? JSON.stringify(body) : '';\n const bodyHash = sha256Hex(bodyStr);\n\n const signStr = [method, path, String(timestamp), bodyHash, credential.accessKey].join('\\n');\n const signature = hmacSha256(credential.secretKey, signStr);\n\n return {\n 'X-Access-Key': credential.accessKey,\n 'X-Timestamp': String(timestamp),\n 'X-Nonce': nonce,\n 'X-Signature': signature,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/platform.ts","../src/sign.ts"],"sourcesContent":["/**\n * @zhimakechuang/game-sdk\n *\n * game-factory 平台专属游戏 SDK,封装 game-server 所有 HTTP 接口。\n *\n * 用法(模板开发 + 生产运行统一入口):\n * import { createGameSdk } from '@zhimakechuang/game-sdk';\n * const sdk = createGameSdk({\n * templateId: 'tpl_xxx', // 模板 ID\n * accessKey: 'ak_tpl_xxx', // 平台入驻后颁发的凭证\n * secretKey: 'sk_tpl_xxx', // 平台入驻后颁发的密钥\n * });\n * await sdk.auth.login({ code: 'xxx', platform: 'wx' });\n * await sdk.score.submit({ score: 1000 });\n *\n * 用法(CDN):\n * <script src=\"https://unpkg.com/@zhimakechuang/game-sdk/dist/index.global.js\"></script>\n * const sdk = GameSDK.createGameSdk({ templateId, accessKey, secretKey });\n *\n * 用法(模拟模式,模板开发调试,不连服务端):\n * const sdk = createGameSdk({ templateId, accessKey, secretKey, mock: true });\n *\n * 鉴权说明:\n * - accessKey/secretKey 必须传入,只有平台入驻开发者才能获取\n * - gameId 由平台构建时注入到 __GAME_CONFIG__,SDK 自动读取,开发者无需传入\n * - baseUrl 内置默认值,指向平台 API 服务\n */\n\nimport {\n GameSdkConfig, LoginOptions, LoginResult, PlayerProfile,\n SubmitScoreOptions, SubmitScoreResult,\n LeaderboardEntry, MyRank,\n} from './types';\nimport { request, storageGet, storageSet, storageRemove } from './platform';\nimport { buildSignHeaders } from './sign';\n\n// 运行时由平台构建时注入的配置(esbuild define)\n// gameId: 游戏实例 ID(用户创建游戏后由平台注入)\n// baseUrl: API 服务地址(平台构建时注入,开发者无需配置)\n// 凭证(accessKey/secretKey)由开发者在 createGameSdk 时传入,不通过此渠道注入\ndeclare const __GAME_CONFIG__: {\n gameId?: string;\n baseUrl?: string;\n} | undefined;\n\n// re-export 所有类型,供外部使用\nexport * from './types';\n\n// ─── 错误 ──────────────────────────────────────────────\n\nexport class GameSdkError extends Error {\n constructor(\n message: string,\n public status: number,\n public data?: any,\n ) {\n super(message);\n this.name = 'GameSdkError';\n }\n}\n\n// ─── SDK 接口定义 ───────────────────────────────────────\n\nexport interface GameSdk {\n /** 身份与登录 */\n auth: {\n /** 登录(微信/抖音/访客),成功后自动持久化 token */\n login(opts: LoginOptions): Promise<LoginResult>;\n /** 获取当前登录玩家信息(需先 login) */\n me(): Promise<PlayerProfile>;\n /** 登出,清除本地 token */\n logout(): void;\n /** 获取当前 token(用于调试) */\n getToken(): string | null;\n };\n /** 分数 */\n score: {\n /** 提交分数(需登录) */\n submit(opts: SubmitScoreOptions): Promise<SubmitScoreResult>;\n };\n /** 排行榜 */\n leaderboard: {\n /** 获取 Top N(匿名可访问,limit 默认 100) */\n top(limit?: number): Promise<LeaderboardEntry[]>;\n /** 获取当前玩家排名(需登录) */\n myRank(): Promise<MyRank>;\n /** 获取附近排名(需登录,range 默认 ±5) */\n nearby(range?: number): Promise<LeaderboardEntry[]>;\n };\n /** 玩家档案 */\n player: {\n /** 获取当前玩家档案(需登录) */\n getProfile(): Promise<PlayerProfile>;\n /** 更新档案(昵称/头像/扩展字段) */\n updateProfile(patch: {\n nickname?: string;\n avatarUrl?: string;\n extra?: Record<string, any>;\n }): Promise<PlayerProfile>;\n };\n /** 云存档(KV) */\n save: {\n /** 保存数据到指定 key */\n set(key: string, value: Record<string, any>): Promise<{ success: boolean; key: string; value: Record<string, any> }>;\n /** 读取指定 key 的数据 */\n get(key: string): Promise<Record<string, any> | null>;\n /** 读取所有存档 */\n getAll(): Promise<Record<string, any>>;\n /** 删除指定 key */\n remove(key: string): Promise<{ success: boolean; key: string }>;\n /** 清空所有存档 */\n clear(): Promise<{ success: boolean; deleted: number }>;\n };\n}\n\n// ─── 创建 SDK ───────────────────────────────────────────\n\nexport function createGameSdk(config: GameSdkConfig): GameSdk {\n // 凭证是必填项,从源头阻止未授权调用\n if (!config.templateId) {\n throw new GameSdkError(\n '缺少 templateId:请在平台创建模板后获取模板 ID',\n 400,\n );\n }\n if (!config.accessKey || !config.secretKey) {\n throw new GameSdkError(\n '缺少 accessKey/secretKey:请确认已在平台入驻并获取开发者凭证',\n 400,\n );\n }\n\n // gameId 和 baseUrl 由平台构建时注入到 __GAME_CONFIG__,运行时自动读取\n // 模板开发阶段(mock 模式)不需要 gameId\n const { gameId, baseUrl } = resolveInjectedConfig();\n\n if (!config.mock && !gameId) {\n throw new GameSdkError(\n '缺少 gameId:请确认游戏已通过平台构建,或使用 mock 模式进行模板开发',\n 400,\n );\n }\n\n // baseUrl 由平台注入,开发者无需配置\n const apiUrl = gameId\n ? `${baseUrl}/game-api/${gameId}`\n : '';\n const tokenKey = `gf_token_${gameId ?? 'mock'}`;\n\n // token 管理(从本地存储恢复)\n let token: string | null = storageGet(tokenKey);\n\n function getToken(): string | null {\n return token;\n }\n\n function setToken(t: string | null): void {\n token = t;\n if (t) storageSet(tokenKey, t);\n else storageRemove(tokenKey);\n }\n\n // ── 内部 HTTP 封装 ──────────────────────────────────\n async function call<T = any>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n body?: any,\n ): Promise<T> {\n if (config.mock) {\n return mockResponse<T>(method, path, body);\n }\n\n const headers: Record<string, string> = {};\n\n // 附加请求签名(HMAC-SHA256,防篡改 + 防重放)\n const signHeaders = buildSignHeaders(\n method,\n path,\n body,\n { accessKey: config.accessKey, secretKey: config.secretKey },\n );\n Object.assign(headers, signHeaders);\n\n // 附加玩家 token\n if (token) headers['Authorization'] = `Bearer ${token}`;\n\n const res = await request<T>({\n url: `${apiUrl}${path}`,\n method,\n headers,\n body,\n });\n\n if (res.status >= 400) {\n const msg = (res.data as any)?.message || `HTTP ${res.status}`;\n throw new GameSdkError(msg, res.status, res.data);\n }\n return res.data;\n }\n\n // ── 方法面 ──────────────────────────────────────────\n const sdk: GameSdk = {\n auth: {\n async login(opts: LoginOptions): Promise<LoginResult> {\n const result = await call<LoginResult>('POST', '/auth/login', opts);\n setToken(result.token);\n return result;\n },\n me: () => call<PlayerProfile>('GET', '/auth/me'),\n logout: () => setToken(null),\n getToken,\n },\n\n score: {\n submit: (opts: SubmitScoreOptions) => call<SubmitScoreResult>('POST', '/score/submit', opts),\n },\n\n leaderboard: {\n top: (limit?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard?limit=${limit || 100}`),\n myRank: () => call<MyRank>('GET', '/leaderboard/me'),\n nearby: (range?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard/nearby?range=${range || 5}`),\n },\n\n player: {\n getProfile: () => call<PlayerProfile>('GET', '/player/profile'),\n updateProfile: (patch) => call<PlayerProfile>('PATCH', '/player/profile', patch),\n },\n\n save: {\n set: (key, value) => call('POST', `/storage/${key}`, { value }),\n get: (key) => call('GET', `/storage/${key}`),\n getAll: () => call('GET', '/storage'),\n remove: (key) => call('DELETE', `/storage/${key}`),\n clear: () => call('DELETE', '/storage'),\n },\n };\n\n return sdk;\n}\n\n// ─── 运行时注入配置解析 ─────────────────────────────────\n\n/**\n * 从 __GAME_CONFIG__ 读取平台构建时注入的 gameId 和 baseUrl。\n *\n * gameId: 游戏实例 ID,只有用户通过平台创建游戏并构建后才会存在。\n * 模板开发阶段(mock 模式)不需要 gameId。\n * baseUrl: API 服务地址,由平台构建时注入,开发者无需手动配置。\n *\n * __GAME_CONFIG__ 由平台构建时通过 esbuild define 注入:\n * define: { '__GAME_CONFIG__': JSON.stringify({ gameId, baseUrl }) }\n */\nfunction resolveInjectedConfig(): { gameId?: string; baseUrl: string } {\n try {\n // eslint-disable-next-line no-undef\n const injected = (typeof __GAME_CONFIG__ !== 'undefined' ? __GAME_CONFIG__ : {}) as {\n gameId?: string;\n baseUrl?: string;\n };\n return {\n gameId: injected.gameId,\n baseUrl: injected.baseUrl ?? '',\n };\n } catch {\n return { gameId: undefined, baseUrl: '' };\n }\n}\n\n// ─── Mock 模式(模板开发不连服务端)──────────────────────\n\nlet mockScore = 0;\nlet mockSaves: Record<string, Record<string, any>> = {};\nlet mockToken: string | null = null;\n\nasync function mockResponse<T>(method: string, path: string, body: any): Promise<T> {\n await new Promise((r) => setTimeout(r, 50)); // 模拟网络延迟\n\n // auth\n if (path === '/auth/login') {\n mockToken = `mock_token_${Date.now()}`;\n const profile: PlayerProfile = {\n openId: 'mock_openid',\n platform: body?.platform || 'guest',\n nickname: body?.nickname || '测试玩家',\n bestScore: mockScore,\n playCount: 0,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n };\n return { token: mockToken, openId: 'mock_openid', isNew: true, profile } as T;\n }\n if (path === '/auth/me') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // score\n if (path === '/score/submit') {\n const prev = mockScore;\n mockScore = Math.max(mockScore, body?.score || 0);\n return {\n submitted: body?.score || 0,\n isHighScore: (body?.score || 0) > prev,\n previousBest: prev,\n bestScore: mockScore,\n rank: 1,\n } as T;\n }\n\n // leaderboard\n if (path.startsWith('/leaderboard?')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n if (path === '/leaderboard/me') {\n return { rank: 1, score: mockScore, totalPlayers: 1, playCount: 1 } as T;\n }\n if (path.startsWith('/leaderboard/nearby')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n\n // player\n if (path === '/player/profile' && method === 'GET') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // storage\n if (path.startsWith('/storage/') && method === 'POST') {\n const key = path.split('/storage/')[1];\n mockSaves[key] = body?.value || {};\n return { success: true, key, value: mockSaves[key] } as T;\n }\n if (path.startsWith('/storage/') && method === 'GET') {\n const key = path.split('/storage/')[1];\n return (mockSaves[key] || null) as T;\n }\n if (path === '/storage' && method === 'GET') {\n return mockSaves as T;\n }\n if (path.startsWith('/storage/') && method === 'DELETE') {\n const key = path.split('/storage/')[1];\n delete mockSaves[key];\n return { success: true, key } as T;\n }\n if (path === '/storage' && method === 'DELETE') {\n const count = Object.keys(mockSaves).length;\n mockSaves = {};\n return { success: true, deleted: count } as T;\n }\n\n return {} as T;\n}\n","/**\n * 平台适配层\n *\n * 自动检测运行环境(微信小游戏 / 抖音小游戏 / H5),\n * 封装 HTTP 请求和本地存储,对上层提供统一接口。\n */\n\n// 小游戏运行时的全局对象(微信/抖音),H5 下不存在\ndeclare const wx: any;\ndeclare const tt: any;\n\n// ─── 环境检测 ──────────────────────────────────────────\n\ntype Runtime = 'wx' | 'tt' | 'h5';\n\nfunction detectRuntime(): Runtime {\n if (typeof wx !== 'undefined' && typeof (wx as any).request === 'function') return 'wx';\n if (typeof tt !== 'undefined' && typeof (tt as any).request === 'function') return 'tt';\n return 'h5';\n}\n\nconst runtime = detectRuntime();\n\n// ─── HTTP 请求 ─────────────────────────────────────────\n\nexport interface HttpRequest {\n url: string;\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n headers?: Record<string, string>;\n body?: any;\n}\n\nexport interface HttpResponse<T = any> {\n status: number;\n data: T;\n}\n\nexport async function request<T = any>(req: HttpRequest): Promise<HttpResponse<T>> {\n if (runtime === 'wx' || runtime === 'tt') {\n return miniProgramRequest<T>(req);\n }\n return fetchRequest<T>(req);\n}\n\n/** 微信/抖音小游戏请求(wx.request / tt.request) */\nfunction miniProgramRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const api = (runtime === 'wx' ? wx : tt) as any;\n return new Promise((resolve, reject) => {\n api.request({\n url: req.url,\n method: req.method,\n header: req.headers || {},\n data: req.body,\n success: (res: any) => {\n resolve({\n status: res.statusCode,\n data: res.data as T,\n });\n },\n fail: (err: any) => {\n reject(new Error(err?.errMsg || 'request failed'));\n },\n });\n });\n}\n\n/** H5 fetch 请求 */\nasync function fetchRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const res = await fetch(req.url, {\n method: req.method,\n headers: { 'Content-Type': 'application/json', ...req.headers },\n body: req.body !== undefined ? JSON.stringify(req.body) : undefined,\n });\n const data = await res.json().catch(() => ({}));\n return { status: res.status, data: data as T };\n}\n\n// ─── 本地存储(token 持久化)────────────────────────────\n\nexport function storageGet(key: string): string | null {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n const val = api.getStorageSync(key);\n return val || null;\n } catch {\n return null;\n }\n }\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nexport function storageSet(key: string, value: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.setStorageSync(key, value);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.setItem(key, value);\n } catch {\n /* ignore */\n }\n}\n\nexport function storageRemove(key: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.removeStorageSync(key);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n}\n","/**\n * 请求签名工具\n *\n * 参考 AWS S3 / 微信支付 / 高德地图的大厂 SDK 鉴权设计:\n * - 每个请求携带 AccessKey(公开标识)+ 签名(HMAC-SHA256)\n * - 签名内容包含:HTTP方法、路径、时间戳、请求体哈希、AccessKey\n * - SecretKey 仅在本地参与签名计算,不会出现在网络请求中\n *\n * 安全特性:\n * - HMAC 签名 → 请求内容未被篡改\n * - 时间戳 → 请求超过 5 分钟无效(防重放)\n * - Nonce 随机串 → 同一时间窗口内不可重复(防重放)\n */\n\n/** 签名计算所需的凭证 */\nexport interface SignCredential {\n accessKey: string;\n secretKey: string;\n}\n\n/**\n * 计算 body 的 SHA256 哈希(hex)。\n * 小游戏环境无原生 crypto.subtle 同步 API,这里用简化实现兼容多端。\n */\nfunction sha256Hex(input: string): string {\n // 优先使用微信/抖音小游戏的 crypto(同步)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHash) {\n return g.crypto.createHash('sha256').update(input).digest('hex');\n }\n // Node 环境\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHash('sha256').update(input).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简单哈希(非加密安全,仅用于无法获取 crypto 的环境)\n let h1 = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h1 ^= input.charCodeAt(i);\n h1 = Math.imul(h1, 0x01000193);\n }\n return (h1 >>> 0).toString(16).padStart(8, '0').repeat(8);\n}\n\n/**\n * HMAC-SHA256 签名。\n * 优先使用原生 crypto,兜底用纯 JS 实现。\n */\nfunction hmacSha256(key: string, message: string): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHmac) {\n return g.crypto.createHmac('sha256', key).update(message).digest('hex');\n }\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHmac('sha256', key).update(message).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简化 HMAC(非加密安全)\n const blockLen = 64;\n const k = key.length > blockLen ? sha256Hex(key) : key.padEnd(blockLen, '\\0');\n const ipad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x36)).join('');\n const opad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x5c)).join('');\n return sha256Hex(opad + sha256Hex(ipad + message));\n}\n\n/** 生成随机 Nonce(16 位) */\nexport function generateNonce(): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.randomBytes) {\n return g.crypto.randomBytes(8).toString('hex');\n }\n let s = '';\n const chars = '0123456789abcdef';\n for (let i = 0; i < 16; i++) s += chars[Math.floor(Math.random() * 16)];\n return s;\n}\n\n/**\n * 构造请求签名头。\n *\n * 签名算法(HMAC-SHA256):\n * signStr = method + \"\\n\" + path + \"\\n\" + timestamp + \"\\n\" + sha256(body) + \"\\n\" + accessKey\n * signature = HMAC_SHA256(secretKey, signStr)\n *\n * @param method HTTP 方法\n * @param path 请求路径(不含 baseUrl,如 /game-api/xxx/score/submit)\n * @param body 请求体(对象或 undefined)\n * @param credential AK/SK 凭证\n * @returns 签名头对象,可直接合并到请求 headers\n */\nexport function buildSignHeaders(\n method: string,\n path: string,\n body: unknown,\n credential: SignCredential,\n): Record<string, string> {\n const timestamp = Math.floor(Date.now() / 1000);\n const nonce = generateNonce();\n const bodyStr = body !== undefined && body !== null ? JSON.stringify(body) : '';\n const bodyHash = sha256Hex(bodyStr);\n\n const signStr = [method, path, String(timestamp), bodyHash, credential.accessKey].join('\\n');\n const signature = hmacSha256(credential.secretKey, signStr);\n\n return {\n 'X-Access-Key': credential.accessKey,\n 'X-Timestamp': String(timestamp),\n 'X-Nonce': nonce,\n 'X-Signature': signature,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACeA,SAAS,gBAAyB;AAChC,MAAI,OAAO,OAAO,eAAe,OAAQ,GAAW,YAAY,WAAY,QAAO;AACnF,MAAI,OAAO,OAAO,eAAe,OAAQ,GAAW,YAAY,WAAY,QAAO;AACnF,SAAO;AACT;AAEA,IAAM,UAAU,cAAc;AAgB9B,eAAsB,QAAiB,KAA4C;AACjF,MAAI,YAAY,QAAQ,YAAY,MAAM;AACxC,WAAO,mBAAsB,GAAG;AAAA,EAClC;AACA,SAAO,aAAgB,GAAG;AAC5B;AAGA,SAAS,mBAAsB,KAA4C;AACzE,QAAM,MAAO,YAAY,OAAO,KAAK;AACrC,SAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AACtC,QAAI,QAAQ;AAAA,MACV,KAAK,IAAI;AAAA,MACT,QAAQ,IAAI;AAAA,MACZ,QAAQ,IAAI,WAAW,CAAC;AAAA,MACxB,MAAM,IAAI;AAAA,MACV,SAAS,CAAC,QAAa;AACrB,gBAAQ;AAAA,UACN,QAAQ,IAAI;AAAA,UACZ,MAAM,IAAI;AAAA,QACZ,CAAC;AAAA,MACH;AAAA,MACA,MAAM,CAAC,QAAa;AAClB,eAAO,IAAI,MAAM,KAAK,UAAU,gBAAgB,CAAC;AAAA,MACnD;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAGA,eAAe,aAAgB,KAA4C;AACzE,QAAM,MAAM,MAAM,MAAM,IAAI,KAAK;AAAA,IAC/B,QAAQ,IAAI;AAAA,IACZ,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,IAAI,QAAQ;AAAA,IAC9D,MAAM,IAAI,SAAS,SAAY,KAAK,UAAU,IAAI,IAAI,IAAI;AAAA,EAC5D,CAAC;AACD,QAAM,OAAO,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAC9C,SAAO,EAAE,QAAQ,IAAI,QAAQ,KAAgB;AAC/C;AAIO,SAAS,WAAW,KAA4B;AACrD,MAAI,YAAY,QAAQ,YAAY,MAAM;AACxC,UAAM,MAAO,YAAY,OAAO,KAAK;AACrC,QAAI;AACF,YAAM,MAAM,IAAI,eAAe,GAAG;AAClC,aAAO,OAAO;AAAA,IAChB,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AACA,MAAI;AACF,WAAO,aAAa,QAAQ,GAAG;AAAA,EACjC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,WAAW,KAAa,OAAqB;AAC3D,MAAI,YAAY,QAAQ,YAAY,MAAM;AACxC,UAAM,MAAO,YAAY,OAAO,KAAK;AACrC,QAAI;AACF,UAAI,eAAe,KAAK,KAAK;AAAA,IAC/B,QAAQ;AAAA,IAER;AACA;AAAA,EACF;AACA,MAAI;AACF,iBAAa,QAAQ,KAAK,KAAK;AAAA,EACjC,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,cAAc,KAAmB;AAC/C,MAAI,YAAY,QAAQ,YAAY,MAAM;AACxC,UAAM,MAAO,YAAY,OAAO,KAAK;AACrC,QAAI;AACF,UAAI,kBAAkB,GAAG;AAAA,IAC3B,QAAQ;AAAA,IAER;AACA;AAAA,EACF;AACA,MAAI;AACF,iBAAa,WAAW,GAAG;AAAA,EAC7B,QAAQ;AAAA,EAER;AACF;;;ACxGA,SAAS,UAAU,OAAuB;AAGxC,QAAM,IAAI;AACV,MAAI,EAAE,QAAQ,YAAY;AACxB,WAAO,EAAE,OAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAAA,EACjE;AAEA,MAAI,OAAO,YAAY,YAAY;AACjC,QAAI;AAEF,YAAM,aAAa,QAAQ,QAAQ;AACnC,aAAO,WAAW,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AAAA,IACnE,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,MAAI,KAAK;AACT,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,MAAM,WAAW,CAAC;AACxB,SAAK,KAAK,KAAK,IAAI,QAAU;AAAA,EAC/B;AACA,UAAQ,OAAO,GAAG,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,EAAE,OAAO,CAAC;AAC1D;AAMA,SAAS,WAAW,KAAa,SAAyB;AAExD,QAAM,IAAI;AACV,MAAI,EAAE,QAAQ,YAAY;AACxB,WAAO,EAAE,OAAO,WAAW,UAAU,GAAG,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAAA,EACxE;AACA,MAAI,OAAO,YAAY,YAAY;AACjC,QAAI;AAEF,YAAM,aAAa,QAAQ,QAAQ;AACnC,aAAO,WAAW,WAAW,UAAU,GAAG,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK;AAAA,IAC1E,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,WAAW;AACjB,QAAM,IAAI,IAAI,SAAS,WAAW,UAAU,GAAG,IAAI,IAAI,OAAO,UAAU,IAAI;AAC5E,QAAM,OAAO,MAAM,KAAK,CAAC,EAAE,IAAI,CAAC,MAAM,OAAO,aAAa,EAAE,WAAW,CAAC,IAAI,EAAI,CAAC,EAAE,KAAK,EAAE;AAC1F,QAAM,OAAO,MAAM,KAAK,CAAC,EAAE,IAAI,CAAC,MAAM,OAAO,aAAa,EAAE,WAAW,CAAC,IAAI,EAAI,CAAC,EAAE,KAAK,EAAE;AAC1F,SAAO,UAAU,OAAO,UAAU,OAAO,OAAO,CAAC;AACnD;AAGO,SAAS,gBAAwB;AAEtC,QAAM,IAAI;AACV,MAAI,EAAE,QAAQ,aAAa;AACzB,WAAO,EAAE,OAAO,YAAY,CAAC,EAAE,SAAS,KAAK;AAAA,EAC/C;AACA,MAAI,IAAI;AACR,QAAM,QAAQ;AACd,WAAS,IAAI,GAAG,IAAI,IAAI,IAAK,MAAK,MAAM,KAAK,MAAM,KAAK,OAAO,IAAI,EAAE,CAAC;AACtE,SAAO;AACT;AAeO,SAAS,iBACd,QACA,MACA,MACA,YACwB;AACxB,QAAM,YAAY,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AAC9C,QAAM,QAAQ,cAAc;AAC5B,QAAM,UAAU,SAAS,UAAa,SAAS,OAAO,KAAK,UAAU,IAAI,IAAI;AAC7E,QAAM,WAAW,UAAU,OAAO;AAElC,QAAM,UAAU,CAAC,QAAQ,MAAM,OAAO,SAAS,GAAG,UAAU,WAAW,SAAS,EAAE,KAAK,IAAI;AAC3F,QAAM,YAAY,WAAW,WAAW,WAAW,OAAO;AAE1D,SAAO;AAAA,IACL,gBAAgB,WAAW;AAAA,IAC3B,eAAe,OAAO,SAAS;AAAA,IAC/B,WAAW;AAAA,IACX,eAAe;AAAA,EACjB;AACF;;;AFzEO,IAAM,eAAN,cAA2B,MAAM;AAAA,EACtC,YACE,SACO,QACA,MACP;AACA,UAAM,OAAO;AAHN;AACA;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AA0DO,SAAS,cAAc,QAAgC;AAE5D,MAAI,CAAC,OAAO,YAAY;AACtB,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,OAAO,aAAa,CAAC,OAAO,WAAW;AAC1C,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAIA,QAAM,EAAE,QAAQ,QAAQ,IAAI,sBAAsB;AAElD,MAAI,CAAC,OAAO,QAAQ,CAAC,QAAQ;AAC3B,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAGA,QAAM,SAAS,SACX,GAAG,OAAO,aAAa,MAAM,KAC7B;AACJ,QAAM,WAAW,YAAY,UAAU,MAAM;AAG7C,MAAI,QAAuB,WAAW,QAAQ;AAE9C,WAAS,WAA0B;AACjC,WAAO;AAAA,EACT;AAEA,WAAS,SAAS,GAAwB;AACxC,YAAQ;AACR,QAAI,EAAG,YAAW,UAAU,CAAC;AAAA,QACxB,eAAc,QAAQ;AAAA,EAC7B;AAGA,iBAAe,KACb,QACA,MACA,MACY;AACZ,QAAI,OAAO,MAAM;AACf,aAAO,aAAgB,QAAQ,MAAM,IAAI;AAAA,IAC3C;AAEA,UAAM,UAAkC,CAAC;AAGzC,UAAM,cAAc;AAAA,MAClB;AAAA,MACA;AAAA,MACA;AAAA,MACA,EAAE,WAAW,OAAO,WAAW,WAAW,OAAO,UAAU;AAAA,IAC7D;AACA,WAAO,OAAO,SAAS,WAAW;AAGlC,QAAI,MAAO,SAAQ,eAAe,IAAI,UAAU,KAAK;AAErD,UAAM,MAAM,MAAM,QAAW;AAAA,MAC3B,KAAK,GAAG,MAAM,GAAG,IAAI;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,QAAI,IAAI,UAAU,KAAK;AACrB,YAAM,MAAO,IAAI,MAAc,WAAW,QAAQ,IAAI,MAAM;AAC5D,YAAM,IAAI,aAAa,KAAK,IAAI,QAAQ,IAAI,IAAI;AAAA,IAClD;AACA,WAAO,IAAI;AAAA,EACb;AAGA,QAAM,MAAe;AAAA,IACnB,MAAM;AAAA,MACJ,MAAM,MAAM,MAA0C;AACpD,cAAM,SAAS,MAAM,KAAkB,QAAQ,eAAe,IAAI;AAClE,iBAAS,OAAO,KAAK;AACrB,eAAO;AAAA,MACT;AAAA,MACA,IAAI,MAAM,KAAoB,OAAO,UAAU;AAAA,MAC/C,QAAQ,MAAM,SAAS,IAAI;AAAA,MAC3B;AAAA,IACF;AAAA,IAEA,OAAO;AAAA,MACL,QAAQ,CAAC,SAA6B,KAAwB,QAAQ,iBAAiB,IAAI;AAAA,IAC7F;AAAA,IAEA,aAAa;AAAA,MACX,KAAK,CAAC,UACJ,KAAyB,OAAO,sBAAsB,SAAS,GAAG,EAAE;AAAA,MACtE,QAAQ,MAAM,KAAa,OAAO,iBAAiB;AAAA,MACnD,QAAQ,CAAC,UACP,KAAyB,OAAO,6BAA6B,SAAS,CAAC,EAAE;AAAA,IAC7E;AAAA,IAEA,QAAQ;AAAA,MACN,YAAY,MAAM,KAAoB,OAAO,iBAAiB;AAAA,MAC9D,eAAe,CAAC,UAAU,KAAoB,SAAS,mBAAmB,KAAK;AAAA,IACjF;AAAA,IAEA,MAAM;AAAA,MACJ,KAAK,CAAC,KAAK,UAAU,KAAK,QAAQ,YAAY,GAAG,IAAI,EAAE,MAAM,CAAC;AAAA,MAC9D,KAAK,CAAC,QAAQ,KAAK,OAAO,YAAY,GAAG,EAAE;AAAA,MAC3C,QAAQ,MAAM,KAAK,OAAO,UAAU;AAAA,MACpC,QAAQ,CAAC,QAAQ,KAAK,UAAU,YAAY,GAAG,EAAE;AAAA,MACjD,OAAO,MAAM,KAAK,UAAU,UAAU;AAAA,IACxC;AAAA,EACF;AAEA,SAAO;AACT;AAcA,SAAS,wBAA8D;AACrE,MAAI;AAEF,UAAM,WAAY,OAAO,oBAAoB,cAAc,kBAAkB,CAAC;AAI9E,WAAO;AAAA,MACL,QAAQ,SAAS;AAAA,MACjB,SAAS,SAAS,WAAW;AAAA,IAC/B;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,QAAQ,QAAW,SAAS,GAAG;AAAA,EAC1C;AACF;AAIA,IAAI,YAAY;AAChB,IAAI,YAAiD,CAAC;AACtD,IAAI,YAA2B;AAE/B,eAAe,aAAgB,QAAgB,MAAc,MAAuB;AAClF,QAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,EAAE,CAAC;AAG1C,MAAI,SAAS,eAAe;AAC1B,gBAAY,cAAc,KAAK,IAAI,CAAC;AACpC,UAAM,UAAyB;AAAA,MAC7B,QAAQ;AAAA,MACR,UAAU,MAAM,YAAY;AAAA,MAC5B,UAAU,MAAM,YAAY;AAAA,MAC5B,WAAW;AAAA,MACX,WAAW;AAAA,MACX,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IACrC;AACA,WAAO,EAAE,OAAO,WAAW,QAAQ,eAAe,OAAO,MAAM,QAAQ;AAAA,EACzE;AACA,MAAI,SAAS,YAAY;AACvB,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,UAAU;AAAA,MACV,WAAW;AAAA,MACX,WAAW;AAAA,MACX,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IACrC;AAAA,EACF;AAGA,MAAI,SAAS,iBAAiB;AAC5B,UAAM,OAAO;AACb,gBAAY,KAAK,IAAI,WAAW,MAAM,SAAS,CAAC;AAChD,WAAO;AAAA,MACL,WAAW,MAAM,SAAS;AAAA,MAC1B,cAAc,MAAM,SAAS,KAAK;AAAA,MAClC,cAAc;AAAA,MACd,WAAW;AAAA,MACX,MAAM;AAAA,IACR;AAAA,EACF;AAGA,MAAI,KAAK,WAAW,eAAe,GAAG;AACpC,WAAO,CAAC;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,OAAO;AAAA,MACP,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACA,MAAI,SAAS,mBAAmB;AAC9B,WAAO,EAAE,MAAM,GAAG,OAAO,WAAW,cAAc,GAAG,WAAW,EAAE;AAAA,EACpE;AACA,MAAI,KAAK,WAAW,qBAAqB,GAAG;AAC1C,WAAO,CAAC;AAAA,MACN,MAAM;AAAA,MACN,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,OAAO;AAAA,MACP,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAGA,MAAI,SAAS,qBAAqB,WAAW,OAAO;AAClD,WAAO;AAAA,MACL,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,UAAU;AAAA,MACV,WAAW;AAAA,MACX,WAAW;AAAA,MACX,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,MAClC,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IACrC;AAAA,EACF;AAGA,MAAI,KAAK,WAAW,WAAW,KAAK,WAAW,QAAQ;AACrD,UAAM,MAAM,KAAK,MAAM,WAAW,EAAE,CAAC;AACrC,cAAU,GAAG,IAAI,MAAM,SAAS,CAAC;AACjC,WAAO,EAAE,SAAS,MAAM,KAAK,OAAO,UAAU,GAAG,EAAE;AAAA,EACrD;AACA,MAAI,KAAK,WAAW,WAAW,KAAK,WAAW,OAAO;AACpD,UAAM,MAAM,KAAK,MAAM,WAAW,EAAE,CAAC;AACrC,WAAQ,UAAU,GAAG,KAAK;AAAA,EAC5B;AACA,MAAI,SAAS,cAAc,WAAW,OAAO;AAC3C,WAAO;AAAA,EACT;AACA,MAAI,KAAK,WAAW,WAAW,KAAK,WAAW,UAAU;AACvD,UAAM,MAAM,KAAK,MAAM,WAAW,EAAE,CAAC;AACrC,WAAO,UAAU,GAAG;AACpB,WAAO,EAAE,SAAS,MAAM,IAAI;AAAA,EAC9B;AACA,MAAI,SAAS,cAAc,WAAW,UAAU;AAC9C,UAAM,QAAQ,OAAO,KAAK,SAAS,EAAE;AACrC,gBAAY,CAAC;AACb,WAAO,EAAE,SAAS,MAAM,SAAS,MAAM;AAAA,EACzC;AAEA,SAAO,CAAC;AACV;","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -165,21 +165,27 @@ var GameSdkError = class extends Error {
|
|
|
165
165
|
}
|
|
166
166
|
};
|
|
167
167
|
function createGameSdk(config) {
|
|
168
|
-
|
|
169
|
-
if (!runtimeConfig.mock && !runtimeConfig.gameId) {
|
|
168
|
+
if (!config.templateId) {
|
|
170
169
|
throw new GameSdkError(
|
|
171
|
-
"\u7F3A\u5C11
|
|
170
|
+
"\u7F3A\u5C11 templateId\uFF1A\u8BF7\u5728\u5E73\u53F0\u521B\u5EFA\u6A21\u677F\u540E\u83B7\u53D6\u6A21\u677F ID",
|
|
172
171
|
400
|
|
173
172
|
);
|
|
174
173
|
}
|
|
175
|
-
if (!
|
|
174
|
+
if (!config.accessKey || !config.secretKey) {
|
|
176
175
|
throw new GameSdkError(
|
|
177
|
-
"\u7F3A\u5C11 accessKey/secretKey\uFF1A\u8BF7\u786E\u8BA4\
|
|
176
|
+
"\u7F3A\u5C11 accessKey/secretKey\uFF1A\u8BF7\u786E\u8BA4\u5DF2\u5728\u5E73\u53F0\u5165\u9A7B\u5E76\u83B7\u53D6\u5F00\u53D1\u8005\u51ED\u8BC1",
|
|
178
177
|
400
|
|
179
178
|
);
|
|
180
179
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
180
|
+
const { gameId, baseUrl } = resolveInjectedConfig();
|
|
181
|
+
if (!config.mock && !gameId) {
|
|
182
|
+
throw new GameSdkError(
|
|
183
|
+
"\u7F3A\u5C11 gameId\uFF1A\u8BF7\u786E\u8BA4\u6E38\u620F\u5DF2\u901A\u8FC7\u5E73\u53F0\u6784\u5EFA\uFF0C\u6216\u4F7F\u7528 mock \u6A21\u5F0F\u8FDB\u884C\u6A21\u677F\u5F00\u53D1",
|
|
184
|
+
400
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const apiUrl = gameId ? `${baseUrl}/game-api/${gameId}` : "";
|
|
188
|
+
const tokenKey = `gf_token_${gameId ?? "mock"}`;
|
|
183
189
|
let token = storageGet(tokenKey);
|
|
184
190
|
function getToken() {
|
|
185
191
|
return token;
|
|
@@ -190,19 +196,17 @@ function createGameSdk(config) {
|
|
|
190
196
|
else storageRemove(tokenKey);
|
|
191
197
|
}
|
|
192
198
|
async function call(method, path, body) {
|
|
193
|
-
if (
|
|
199
|
+
if (config.mock) {
|
|
194
200
|
return mockResponse(method, path, body);
|
|
195
201
|
}
|
|
196
202
|
const headers = {};
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
Object.assign(headers, signHeaders);
|
|
205
|
-
}
|
|
203
|
+
const signHeaders = buildSignHeaders(
|
|
204
|
+
method,
|
|
205
|
+
path,
|
|
206
|
+
body,
|
|
207
|
+
{ accessKey: config.accessKey, secretKey: config.secretKey }
|
|
208
|
+
);
|
|
209
|
+
Object.assign(headers, signHeaders);
|
|
206
210
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
207
211
|
const res = await request({
|
|
208
212
|
url: `${apiUrl}${path}`,
|
|
@@ -249,20 +253,16 @@ function createGameSdk(config) {
|
|
|
249
253
|
};
|
|
250
254
|
return sdk;
|
|
251
255
|
}
|
|
252
|
-
function
|
|
253
|
-
let injected = {};
|
|
256
|
+
function resolveInjectedConfig() {
|
|
254
257
|
try {
|
|
255
|
-
injected = typeof __GAME_CONFIG__ !== "undefined" ? __GAME_CONFIG__ : {};
|
|
258
|
+
const injected = typeof __GAME_CONFIG__ !== "undefined" ? __GAME_CONFIG__ : {};
|
|
259
|
+
return {
|
|
260
|
+
gameId: injected.gameId,
|
|
261
|
+
baseUrl: injected.baseUrl ?? ""
|
|
262
|
+
};
|
|
256
263
|
} catch {
|
|
257
|
-
|
|
264
|
+
return { gameId: void 0, baseUrl: "" };
|
|
258
265
|
}
|
|
259
|
-
return {
|
|
260
|
-
gameId: config.gameId ?? injected.gameId,
|
|
261
|
-
baseUrl: config.baseUrl ?? injected.baseUrl ?? "",
|
|
262
|
-
accessKey: config.accessKey ?? injected.accessKey,
|
|
263
|
-
secretKey: config.secretKey ?? injected.secretKey,
|
|
264
|
-
mock: config.mock
|
|
265
|
-
};
|
|
266
266
|
}
|
|
267
267
|
var mockScore = 0;
|
|
268
268
|
var mockSaves = {};
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/platform.ts","../src/sign.ts","../src/index.ts"],"sourcesContent":["/**\n * 平台适配层\n *\n * 自动检测运行环境(微信小游戏 / 抖音小游戏 / H5),\n * 封装 HTTP 请求和本地存储,对上层提供统一接口。\n */\n\n// 小游戏运行时的全局对象(微信/抖音),H5 下不存在\ndeclare const wx: any;\ndeclare const tt: any;\n\n// ─── 环境检测 ──────────────────────────────────────────\n\ntype Runtime = 'wx' | 'tt' | 'h5';\n\nfunction detectRuntime(): Runtime {\n if (typeof wx !== 'undefined' && typeof (wx as any).request === 'function') return 'wx';\n if (typeof tt !== 'undefined' && typeof (tt as any).request === 'function') return 'tt';\n return 'h5';\n}\n\nconst runtime = detectRuntime();\n\n// ─── HTTP 请求 ─────────────────────────────────────────\n\nexport interface HttpRequest {\n url: string;\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n headers?: Record<string, string>;\n body?: any;\n}\n\nexport interface HttpResponse<T = any> {\n status: number;\n data: T;\n}\n\nexport async function request<T = any>(req: HttpRequest): Promise<HttpResponse<T>> {\n if (runtime === 'wx' || runtime === 'tt') {\n return miniProgramRequest<T>(req);\n }\n return fetchRequest<T>(req);\n}\n\n/** 微信/抖音小游戏请求(wx.request / tt.request) */\nfunction miniProgramRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const api = (runtime === 'wx' ? wx : tt) as any;\n return new Promise((resolve, reject) => {\n api.request({\n url: req.url,\n method: req.method,\n header: req.headers || {},\n data: req.body,\n success: (res: any) => {\n resolve({\n status: res.statusCode,\n data: res.data as T,\n });\n },\n fail: (err: any) => {\n reject(new Error(err?.errMsg || 'request failed'));\n },\n });\n });\n}\n\n/** H5 fetch 请求 */\nasync function fetchRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const res = await fetch(req.url, {\n method: req.method,\n headers: { 'Content-Type': 'application/json', ...req.headers },\n body: req.body !== undefined ? JSON.stringify(req.body) : undefined,\n });\n const data = await res.json().catch(() => ({}));\n return { status: res.status, data: data as T };\n}\n\n// ─── 本地存储(token 持久化)────────────────────────────\n\nexport function storageGet(key: string): string | null {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n const val = api.getStorageSync(key);\n return val || null;\n } catch {\n return null;\n }\n }\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nexport function storageSet(key: string, value: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.setStorageSync(key, value);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.setItem(key, value);\n } catch {\n /* ignore */\n }\n}\n\nexport function storageRemove(key: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.removeStorageSync(key);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n}\n","/**\n * 请求签名工具\n *\n * 参考 AWS S3 / 微信支付 / 高德地图的大厂 SDK 鉴权设计:\n * - 每个请求携带 AccessKey(公开标识)+ 签名(HMAC-SHA256)\n * - 签名内容包含:HTTP方法、路径、时间戳、请求体哈希、AccessKey\n * - SecretKey 仅在本地参与签名计算,不会出现在网络请求中\n *\n * 安全特性:\n * - HMAC 签名 → 请求内容未被篡改\n * - 时间戳 → 请求超过 5 分钟无效(防重放)\n * - Nonce 随机串 → 同一时间窗口内不可重复(防重放)\n */\n\n/** 签名计算所需的凭证 */\nexport interface SignCredential {\n accessKey: string;\n secretKey: string;\n}\n\n/**\n * 计算 body 的 SHA256 哈希(hex)。\n * 小游戏环境无原生 crypto.subtle 同步 API,这里用简化实现兼容多端。\n */\nfunction sha256Hex(input: string): string {\n // 优先使用微信/抖音小游戏的 crypto(同步)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHash) {\n return g.crypto.createHash('sha256').update(input).digest('hex');\n }\n // Node 环境\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHash('sha256').update(input).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简单哈希(非加密安全,仅用于无法获取 crypto 的环境)\n let h1 = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h1 ^= input.charCodeAt(i);\n h1 = Math.imul(h1, 0x01000193);\n }\n return (h1 >>> 0).toString(16).padStart(8, '0').repeat(8);\n}\n\n/**\n * HMAC-SHA256 签名。\n * 优先使用原生 crypto,兜底用纯 JS 实现。\n */\nfunction hmacSha256(key: string, message: string): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHmac) {\n return g.crypto.createHmac('sha256', key).update(message).digest('hex');\n }\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHmac('sha256', key).update(message).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简化 HMAC(非加密安全)\n const blockLen = 64;\n const k = key.length > blockLen ? sha256Hex(key) : key.padEnd(blockLen, '\\0');\n const ipad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x36)).join('');\n const opad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x5c)).join('');\n return sha256Hex(opad + sha256Hex(ipad + message));\n}\n\n/** 生成随机 Nonce(16 位) */\nexport function generateNonce(): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.randomBytes) {\n return g.crypto.randomBytes(8).toString('hex');\n }\n let s = '';\n const chars = '0123456789abcdef';\n for (let i = 0; i < 16; i++) s += chars[Math.floor(Math.random() * 16)];\n return s;\n}\n\n/**\n * 构造请求签名头。\n *\n * 签名算法(HMAC-SHA256):\n * signStr = method + \"\\n\" + path + \"\\n\" + timestamp + \"\\n\" + sha256(body) + \"\\n\" + accessKey\n * signature = HMAC_SHA256(secretKey, signStr)\n *\n * @param method HTTP 方法\n * @param path 请求路径(不含 baseUrl,如 /game-api/xxx/score/submit)\n * @param body 请求体(对象或 undefined)\n * @param credential AK/SK 凭证\n * @returns 签名头对象,可直接合并到请求 headers\n */\nexport function buildSignHeaders(\n method: string,\n path: string,\n body: unknown,\n credential: SignCredential,\n): Record<string, string> {\n const timestamp = Math.floor(Date.now() / 1000);\n const nonce = generateNonce();\n const bodyStr = body !== undefined && body !== null ? JSON.stringify(body) : '';\n const bodyHash = sha256Hex(bodyStr);\n\n const signStr = [method, path, String(timestamp), bodyHash, credential.accessKey].join('\\n');\n const signature = hmacSha256(credential.secretKey, signStr);\n\n return {\n 'X-Access-Key': credential.accessKey,\n 'X-Timestamp': String(timestamp),\n 'X-Nonce': nonce,\n 'X-Signature': signature,\n };\n}\n","/**\n * @game-factory/game-sdk\n *\n * 通用游戏 SDK,封装 game-server-shared 所有 HTTP 接口。\n *\n * 用法(工程化):\n * import { createGameSdk } from '@game-factory/game-sdk';\n * const sdk = createGameSdk({ baseUrl: 'https://api.example.com' });\n * await sdk.auth.login({ code: 'xxx', platform: 'wx' });\n * await sdk.score.submit({ score: 1000 });\n *\n * 用法(CDN):\n * <script src=\"https://cdn.example.com/game-sdk/index.umd.js\"></script>\n * const sdk = GameSDK.createGameSdk({ baseUrl: '...' });\n *\n * 用法(模拟模式,不连服务端):\n * const sdk = createGameSdk({ baseUrl: '', mock: true });\n *\n * 凭证说明:\n * gameId / accessKey / secretKey 由平台在构建时注入到 __GAME_CONFIG__,\n * SDK 自动读取,开发者无需手动传入。\n * 模板开发阶段使用 mock 模式,不需要凭证。\n */\n\nimport {\n GameSdkConfig, LoginOptions, LoginResult, PlayerProfile,\n SubmitScoreOptions, SubmitScoreResult,\n LeaderboardEntry, MyRank,\n} from './types';\nimport { request, storageGet, storageSet, storageRemove } from './platform';\nimport { buildSignHeaders } from './sign';\n\n// 运行时由平台构建时注入的配置(esbuild define)\ndeclare const __GAME_CONFIG__: {\n gameId?: string;\n accessKey?: string;\n secretKey?: string;\n baseUrl?: string;\n} | undefined;\n\n// re-export 所有类型,供外部使用\nexport * from './types';\n\n// ─── 错误 ──────────────────────────────────────────────\n\nexport class GameSdkError extends Error {\n constructor(\n message: string,\n public status: number,\n public data?: any,\n ) {\n super(message);\n this.name = 'GameSdkError';\n }\n}\n\n// ─── SDK 接口定义 ───────────────────────────────────────\n\nexport interface GameSdk {\n /** 身份与登录 */\n auth: {\n /** 登录(微信/抖音/访客),成功后自动持久化 token */\n login(opts: LoginOptions): Promise<LoginResult>;\n /** 获取当前登录玩家信息(需先 login) */\n me(): Promise<PlayerProfile>;\n /** 登出,清除本地 token */\n logout(): void;\n /** 获取当前 token(用于调试) */\n getToken(): string | null;\n };\n /** 分数 */\n score: {\n /** 提交分数(需登录) */\n submit(opts: SubmitScoreOptions): Promise<SubmitScoreResult>;\n };\n /** 排行榜 */\n leaderboard: {\n /** 获取 Top N(匿名可访问,limit 默认 100) */\n top(limit?: number): Promise<LeaderboardEntry[]>;\n /** 获取当前玩家排名(需登录) */\n myRank(): Promise<MyRank>;\n /** 获取附近排名(需登录,range 默认 ±5) */\n nearby(range?: number): Promise<LeaderboardEntry[]>;\n };\n /** 玩家档案 */\n player: {\n /** 获取当前玩家档案(需登录) */\n getProfile(): Promise<PlayerProfile>;\n /** 更新档案(昵称/头像/扩展字段) */\n updateProfile(patch: {\n nickname?: string;\n avatarUrl?: string;\n extra?: Record<string, any>;\n }): Promise<PlayerProfile>;\n };\n /** 云存档(KV) */\n save: {\n /** 保存数据到指定 key */\n set(key: string, value: Record<string, any>): Promise<{ success: boolean; key: string; value: Record<string, any> }>;\n /** 读取指定 key 的数据 */\n get(key: string): Promise<Record<string, any> | null>;\n /** 读取所有存档 */\n getAll(): Promise<Record<string, any>>;\n /** 删除指定 key */\n remove(key: string): Promise<{ success: boolean; key: string }>;\n /** 清空所有存档 */\n clear(): Promise<{ success: boolean; deleted: number }>;\n };\n}\n\n// ─── 创建 SDK ───────────────────────────────────────────\n\nexport function createGameSdk(config: GameSdkConfig): GameSdk {\n // 合并 __GAME_CONFIG__ 注入的凭证(构建时注入,运行时自动读取)\n // 优先级:显式传入 > __GAME_CONFIG__ 注入\n const runtimeConfig = resolveRuntimeConfig(config);\n\n // 非 mock 模式必须有 gameId(游戏运行阶段由平台注入)\n if (!runtimeConfig.mock && !runtimeConfig.gameId) {\n throw new GameSdkError(\n '缺少 gameId:请确认游戏已通过平台构建,或使用 mock 模式进行模板开发',\n 400,\n );\n }\n\n // 非 mock 模式必须有凭证(游戏运行阶段由平台注入)\n if (!runtimeConfig.mock && (!runtimeConfig.accessKey || !runtimeConfig.secretKey)) {\n throw new GameSdkError(\n '缺少 accessKey/secretKey:请确认游戏已通过平台构建并颁发了模板级凭证',\n 400,\n );\n }\n\n const apiUrl = runtimeConfig.baseUrl\n ? `${runtimeConfig.baseUrl}/game-api/${runtimeConfig.gameId}`\n : '';\n const tokenKey = `gf_token_${runtimeConfig.gameId ?? 'mock'}`;\n\n // token 管理(从本地存储恢复)\n let token: string | null = storageGet(tokenKey);\n\n function getToken(): string | null {\n return token;\n }\n\n function setToken(t: string | null): void {\n token = t;\n if (t) storageSet(tokenKey, t);\n else storageRemove(tokenKey);\n }\n\n // ── 内部 HTTP 封装 ──────────────────────────────────\n async function call<T = any>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n body?: any,\n ): Promise<T> {\n if (runtimeConfig.mock) {\n return mockResponse<T>(method, path, body);\n }\n\n const headers: Record<string, string> = {};\n\n // 附加请求签名(HMAC-SHA256,防篡改 + 防重放)\n if (runtimeConfig.accessKey && runtimeConfig.secretKey) {\n const signHeaders = buildSignHeaders(\n method,\n path,\n body,\n { accessKey: runtimeConfig.accessKey, secretKey: runtimeConfig.secretKey },\n );\n Object.assign(headers, signHeaders);\n }\n\n // 附加玩家 token\n if (token) headers['Authorization'] = `Bearer ${token}`;\n\n const res = await request<T>({\n url: `${apiUrl}${path}`,\n method,\n headers,\n body,\n });\n\n if (res.status >= 400) {\n const msg = (res.data as any)?.message || `HTTP ${res.status}`;\n throw new GameSdkError(msg, res.status, res.data);\n }\n return res.data;\n }\n\n // ── 方法面 ──────────────────────────────────────────\n const sdk: GameSdk = {\n auth: {\n async login(opts: LoginOptions): Promise<LoginResult> {\n const result = await call<LoginResult>('POST', '/auth/login', opts);\n setToken(result.token);\n return result;\n },\n me: () => call<PlayerProfile>('GET', '/auth/me'),\n logout: () => setToken(null),\n getToken,\n },\n\n score: {\n submit: (opts: SubmitScoreOptions) => call<SubmitScoreResult>('POST', '/score/submit', opts),\n },\n\n leaderboard: {\n top: (limit?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard?limit=${limit || 100}`),\n myRank: () => call<MyRank>('GET', '/leaderboard/me'),\n nearby: (range?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard/nearby?range=${range || 5}`),\n },\n\n player: {\n getProfile: () => call<PlayerProfile>('GET', '/player/profile'),\n updateProfile: (patch) => call<PlayerProfile>('PATCH', '/player/profile', patch),\n },\n\n save: {\n set: (key, value) => call('POST', `/storage/${key}`, { value }),\n get: (key) => call('GET', `/storage/${key}`),\n getAll: () => call('GET', '/storage'),\n remove: (key) => call('DELETE', `/storage/${key}`),\n clear: () => call('DELETE', '/storage'),\n },\n };\n\n return sdk;\n}\n\n// ─── 运行时配置解析 ─────────────────────────────────────\n\n/**\n * 合并用户显式传入的配置与 __GAME_CONFIG__ 注入的配置。\n * 优先级:显式传入 > __GAME_CONFIG__ 注入\n *\n * __GAME_CONFIG__ 由平台构建时通过 esbuild define 注入:\n * define: { '__GAME_CONFIG__': JSON.stringify({ gameId, accessKey, secretKey, baseUrl }) }\n */\nfunction resolveRuntimeConfig(config: GameSdkConfig): GameSdkConfig {\n let injected: Partial<GameSdkConfig> = {};\n try {\n // __GAME_CONFIG__ 可能不存在(模板开发阶段 / CDN 直连)\n // eslint-disable-next-line no-undef\n injected = (typeof __GAME_CONFIG__ !== 'undefined' ? __GAME_CONFIG__ : {}) as any;\n } catch {\n // __GAME_CONFIG__ 未定义时 ReferenceError,忽略\n injected = {};\n }\n\n return {\n gameId: config.gameId ?? injected.gameId,\n baseUrl: config.baseUrl ?? injected.baseUrl ?? '',\n accessKey: config.accessKey ?? injected.accessKey,\n secretKey: config.secretKey ?? injected.secretKey,\n mock: config.mock,\n };\n}\n\n// ─── Mock 模式(模板开发不连服务端)──────────────────────\n\nlet mockScore = 0;\nlet mockSaves: Record<string, Record<string, any>> = {};\nlet mockToken: string | null = null;\n\nasync function mockResponse<T>(method: string, path: string, body: any): Promise<T> {\n await new Promise((r) => setTimeout(r, 50)); // 模拟网络延迟\n\n // auth\n if (path === '/auth/login') {\n mockToken = `mock_token_${Date.now()}`;\n const profile: PlayerProfile = {\n openId: 'mock_openid',\n platform: body?.platform || 'guest',\n nickname: body?.nickname || '测试玩家',\n bestScore: mockScore,\n playCount: 0,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n };\n return { token: mockToken, openId: 'mock_openid', isNew: true, profile } as T;\n }\n if (path === '/auth/me') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // score\n if (path === '/score/submit') {\n const prev = mockScore;\n mockScore = Math.max(mockScore, body?.score || 0);\n return {\n submitted: body?.score || 0,\n isHighScore: (body?.score || 0) > prev,\n previousBest: prev,\n bestScore: mockScore,\n rank: 1,\n } as T;\n }\n\n // leaderboard\n if (path.startsWith('/leaderboard?')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n if (path === '/leaderboard/me') {\n return { rank: 1, score: mockScore, totalPlayers: 1, playCount: 1 } as T;\n }\n if (path.startsWith('/leaderboard/nearby')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n\n // player\n if (path === '/player/profile' && method === 'GET') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // storage\n if (path.startsWith('/storage/') && method === 'POST') {\n const key = path.split('/storage/')[1];\n mockSaves[key] = body?.value || {};\n return { success: true, key, value: mockSaves[key] } as T;\n }\n if (path.startsWith('/storage/') && method === 'GET') {\n const key = path.split('/storage/')[1];\n return (mockSaves[key] || null) as T;\n }\n if (path === '/storage' && method === 'GET') {\n return mockSaves as T;\n }\n if (path.startsWith('/storage/') && method === 'DELETE') {\n const key = path.split('/storage/')[1];\n delete mockSaves[key];\n return { success: true, key } as T;\n }\n if (path === '/storage' && method === 'DELETE') {\n const count = Object.keys(mockSaves).length;\n mockSaves = {};\n return { success: true, deleted: count } as T;\n }\n\n return {} as T;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/platform.ts","../src/sign.ts","../src/index.ts"],"sourcesContent":["/**\n * 平台适配层\n *\n * 自动检测运行环境(微信小游戏 / 抖音小游戏 / H5),\n * 封装 HTTP 请求和本地存储,对上层提供统一接口。\n */\n\n// 小游戏运行时的全局对象(微信/抖音),H5 下不存在\ndeclare const wx: any;\ndeclare const tt: any;\n\n// ─── 环境检测 ──────────────────────────────────────────\n\ntype Runtime = 'wx' | 'tt' | 'h5';\n\nfunction detectRuntime(): Runtime {\n if (typeof wx !== 'undefined' && typeof (wx as any).request === 'function') return 'wx';\n if (typeof tt !== 'undefined' && typeof (tt as any).request === 'function') return 'tt';\n return 'h5';\n}\n\nconst runtime = detectRuntime();\n\n// ─── HTTP 请求 ─────────────────────────────────────────\n\nexport interface HttpRequest {\n url: string;\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n headers?: Record<string, string>;\n body?: any;\n}\n\nexport interface HttpResponse<T = any> {\n status: number;\n data: T;\n}\n\nexport async function request<T = any>(req: HttpRequest): Promise<HttpResponse<T>> {\n if (runtime === 'wx' || runtime === 'tt') {\n return miniProgramRequest<T>(req);\n }\n return fetchRequest<T>(req);\n}\n\n/** 微信/抖音小游戏请求(wx.request / tt.request) */\nfunction miniProgramRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const api = (runtime === 'wx' ? wx : tt) as any;\n return new Promise((resolve, reject) => {\n api.request({\n url: req.url,\n method: req.method,\n header: req.headers || {},\n data: req.body,\n success: (res: any) => {\n resolve({\n status: res.statusCode,\n data: res.data as T,\n });\n },\n fail: (err: any) => {\n reject(new Error(err?.errMsg || 'request failed'));\n },\n });\n });\n}\n\n/** H5 fetch 请求 */\nasync function fetchRequest<T>(req: HttpRequest): Promise<HttpResponse<T>> {\n const res = await fetch(req.url, {\n method: req.method,\n headers: { 'Content-Type': 'application/json', ...req.headers },\n body: req.body !== undefined ? JSON.stringify(req.body) : undefined,\n });\n const data = await res.json().catch(() => ({}));\n return { status: res.status, data: data as T };\n}\n\n// ─── 本地存储(token 持久化)────────────────────────────\n\nexport function storageGet(key: string): string | null {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n const val = api.getStorageSync(key);\n return val || null;\n } catch {\n return null;\n }\n }\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n}\n\nexport function storageSet(key: string, value: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.setStorageSync(key, value);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.setItem(key, value);\n } catch {\n /* ignore */\n }\n}\n\nexport function storageRemove(key: string): void {\n if (runtime === 'wx' || runtime === 'tt') {\n const api = (runtime === 'wx' ? wx : tt) as any;\n try {\n api.removeStorageSync(key);\n } catch {\n /* ignore */\n }\n return;\n }\n try {\n localStorage.removeItem(key);\n } catch {\n /* ignore */\n }\n}\n","/**\n * 请求签名工具\n *\n * 参考 AWS S3 / 微信支付 / 高德地图的大厂 SDK 鉴权设计:\n * - 每个请求携带 AccessKey(公开标识)+ 签名(HMAC-SHA256)\n * - 签名内容包含:HTTP方法、路径、时间戳、请求体哈希、AccessKey\n * - SecretKey 仅在本地参与签名计算,不会出现在网络请求中\n *\n * 安全特性:\n * - HMAC 签名 → 请求内容未被篡改\n * - 时间戳 → 请求超过 5 分钟无效(防重放)\n * - Nonce 随机串 → 同一时间窗口内不可重复(防重放)\n */\n\n/** 签名计算所需的凭证 */\nexport interface SignCredential {\n accessKey: string;\n secretKey: string;\n}\n\n/**\n * 计算 body 的 SHA256 哈希(hex)。\n * 小游戏环境无原生 crypto.subtle 同步 API,这里用简化实现兼容多端。\n */\nfunction sha256Hex(input: string): string {\n // 优先使用微信/抖音小游戏的 crypto(同步)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHash) {\n return g.crypto.createHash('sha256').update(input).digest('hex');\n }\n // Node 环境\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHash('sha256').update(input).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简单哈希(非加密安全,仅用于无法获取 crypto 的环境)\n let h1 = 0x811c9dc5;\n for (let i = 0; i < input.length; i++) {\n h1 ^= input.charCodeAt(i);\n h1 = Math.imul(h1, 0x01000193);\n }\n return (h1 >>> 0).toString(16).padStart(8, '0').repeat(8);\n}\n\n/**\n * HMAC-SHA256 签名。\n * 优先使用原生 crypto,兜底用纯 JS 实现。\n */\nfunction hmacSha256(key: string, message: string): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.createHmac) {\n return g.crypto.createHmac('sha256', key).update(message).digest('hex');\n }\n if (typeof require === 'function') {\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const nodeCrypto = require('crypto');\n return nodeCrypto.createHmac('sha256', key).update(message).digest('hex');\n } catch {\n /* fall through */\n }\n }\n // 兜底:简化 HMAC(非加密安全)\n const blockLen = 64;\n const k = key.length > blockLen ? sha256Hex(key) : key.padEnd(blockLen, '\\0');\n const ipad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x36)).join('');\n const opad = Array.from(k).map((c) => String.fromCharCode(c.charCodeAt(0) ^ 0x5c)).join('');\n return sha256Hex(opad + sha256Hex(ipad + message));\n}\n\n/** 生成随机 Nonce(16 位) */\nexport function generateNonce(): string {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const g = globalThis as any;\n if (g.crypto?.randomBytes) {\n return g.crypto.randomBytes(8).toString('hex');\n }\n let s = '';\n const chars = '0123456789abcdef';\n for (let i = 0; i < 16; i++) s += chars[Math.floor(Math.random() * 16)];\n return s;\n}\n\n/**\n * 构造请求签名头。\n *\n * 签名算法(HMAC-SHA256):\n * signStr = method + \"\\n\" + path + \"\\n\" + timestamp + \"\\n\" + sha256(body) + \"\\n\" + accessKey\n * signature = HMAC_SHA256(secretKey, signStr)\n *\n * @param method HTTP 方法\n * @param path 请求路径(不含 baseUrl,如 /game-api/xxx/score/submit)\n * @param body 请求体(对象或 undefined)\n * @param credential AK/SK 凭证\n * @returns 签名头对象,可直接合并到请求 headers\n */\nexport function buildSignHeaders(\n method: string,\n path: string,\n body: unknown,\n credential: SignCredential,\n): Record<string, string> {\n const timestamp = Math.floor(Date.now() / 1000);\n const nonce = generateNonce();\n const bodyStr = body !== undefined && body !== null ? JSON.stringify(body) : '';\n const bodyHash = sha256Hex(bodyStr);\n\n const signStr = [method, path, String(timestamp), bodyHash, credential.accessKey].join('\\n');\n const signature = hmacSha256(credential.secretKey, signStr);\n\n return {\n 'X-Access-Key': credential.accessKey,\n 'X-Timestamp': String(timestamp),\n 'X-Nonce': nonce,\n 'X-Signature': signature,\n };\n}\n","/**\n * @zhimakechuang/game-sdk\n *\n * game-factory 平台专属游戏 SDK,封装 game-server 所有 HTTP 接口。\n *\n * 用法(模板开发 + 生产运行统一入口):\n * import { createGameSdk } from '@zhimakechuang/game-sdk';\n * const sdk = createGameSdk({\n * templateId: 'tpl_xxx', // 模板 ID\n * accessKey: 'ak_tpl_xxx', // 平台入驻后颁发的凭证\n * secretKey: 'sk_tpl_xxx', // 平台入驻后颁发的密钥\n * });\n * await sdk.auth.login({ code: 'xxx', platform: 'wx' });\n * await sdk.score.submit({ score: 1000 });\n *\n * 用法(CDN):\n * <script src=\"https://unpkg.com/@zhimakechuang/game-sdk/dist/index.global.js\"></script>\n * const sdk = GameSDK.createGameSdk({ templateId, accessKey, secretKey });\n *\n * 用法(模拟模式,模板开发调试,不连服务端):\n * const sdk = createGameSdk({ templateId, accessKey, secretKey, mock: true });\n *\n * 鉴权说明:\n * - accessKey/secretKey 必须传入,只有平台入驻开发者才能获取\n * - gameId 由平台构建时注入到 __GAME_CONFIG__,SDK 自动读取,开发者无需传入\n * - baseUrl 内置默认值,指向平台 API 服务\n */\n\nimport {\n GameSdkConfig, LoginOptions, LoginResult, PlayerProfile,\n SubmitScoreOptions, SubmitScoreResult,\n LeaderboardEntry, MyRank,\n} from './types';\nimport { request, storageGet, storageSet, storageRemove } from './platform';\nimport { buildSignHeaders } from './sign';\n\n// 运行时由平台构建时注入的配置(esbuild define)\n// gameId: 游戏实例 ID(用户创建游戏后由平台注入)\n// baseUrl: API 服务地址(平台构建时注入,开发者无需配置)\n// 凭证(accessKey/secretKey)由开发者在 createGameSdk 时传入,不通过此渠道注入\ndeclare const __GAME_CONFIG__: {\n gameId?: string;\n baseUrl?: string;\n} | undefined;\n\n// re-export 所有类型,供外部使用\nexport * from './types';\n\n// ─── 错误 ──────────────────────────────────────────────\n\nexport class GameSdkError extends Error {\n constructor(\n message: string,\n public status: number,\n public data?: any,\n ) {\n super(message);\n this.name = 'GameSdkError';\n }\n}\n\n// ─── SDK 接口定义 ───────────────────────────────────────\n\nexport interface GameSdk {\n /** 身份与登录 */\n auth: {\n /** 登录(微信/抖音/访客),成功后自动持久化 token */\n login(opts: LoginOptions): Promise<LoginResult>;\n /** 获取当前登录玩家信息(需先 login) */\n me(): Promise<PlayerProfile>;\n /** 登出,清除本地 token */\n logout(): void;\n /** 获取当前 token(用于调试) */\n getToken(): string | null;\n };\n /** 分数 */\n score: {\n /** 提交分数(需登录) */\n submit(opts: SubmitScoreOptions): Promise<SubmitScoreResult>;\n };\n /** 排行榜 */\n leaderboard: {\n /** 获取 Top N(匿名可访问,limit 默认 100) */\n top(limit?: number): Promise<LeaderboardEntry[]>;\n /** 获取当前玩家排名(需登录) */\n myRank(): Promise<MyRank>;\n /** 获取附近排名(需登录,range 默认 ±5) */\n nearby(range?: number): Promise<LeaderboardEntry[]>;\n };\n /** 玩家档案 */\n player: {\n /** 获取当前玩家档案(需登录) */\n getProfile(): Promise<PlayerProfile>;\n /** 更新档案(昵称/头像/扩展字段) */\n updateProfile(patch: {\n nickname?: string;\n avatarUrl?: string;\n extra?: Record<string, any>;\n }): Promise<PlayerProfile>;\n };\n /** 云存档(KV) */\n save: {\n /** 保存数据到指定 key */\n set(key: string, value: Record<string, any>): Promise<{ success: boolean; key: string; value: Record<string, any> }>;\n /** 读取指定 key 的数据 */\n get(key: string): Promise<Record<string, any> | null>;\n /** 读取所有存档 */\n getAll(): Promise<Record<string, any>>;\n /** 删除指定 key */\n remove(key: string): Promise<{ success: boolean; key: string }>;\n /** 清空所有存档 */\n clear(): Promise<{ success: boolean; deleted: number }>;\n };\n}\n\n// ─── 创建 SDK ───────────────────────────────────────────\n\nexport function createGameSdk(config: GameSdkConfig): GameSdk {\n // 凭证是必填项,从源头阻止未授权调用\n if (!config.templateId) {\n throw new GameSdkError(\n '缺少 templateId:请在平台创建模板后获取模板 ID',\n 400,\n );\n }\n if (!config.accessKey || !config.secretKey) {\n throw new GameSdkError(\n '缺少 accessKey/secretKey:请确认已在平台入驻并获取开发者凭证',\n 400,\n );\n }\n\n // gameId 和 baseUrl 由平台构建时注入到 __GAME_CONFIG__,运行时自动读取\n // 模板开发阶段(mock 模式)不需要 gameId\n const { gameId, baseUrl } = resolveInjectedConfig();\n\n if (!config.mock && !gameId) {\n throw new GameSdkError(\n '缺少 gameId:请确认游戏已通过平台构建,或使用 mock 模式进行模板开发',\n 400,\n );\n }\n\n // baseUrl 由平台注入,开发者无需配置\n const apiUrl = gameId\n ? `${baseUrl}/game-api/${gameId}`\n : '';\n const tokenKey = `gf_token_${gameId ?? 'mock'}`;\n\n // token 管理(从本地存储恢复)\n let token: string | null = storageGet(tokenKey);\n\n function getToken(): string | null {\n return token;\n }\n\n function setToken(t: string | null): void {\n token = t;\n if (t) storageSet(tokenKey, t);\n else storageRemove(tokenKey);\n }\n\n // ── 内部 HTTP 封装 ──────────────────────────────────\n async function call<T = any>(\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',\n path: string,\n body?: any,\n ): Promise<T> {\n if (config.mock) {\n return mockResponse<T>(method, path, body);\n }\n\n const headers: Record<string, string> = {};\n\n // 附加请求签名(HMAC-SHA256,防篡改 + 防重放)\n const signHeaders = buildSignHeaders(\n method,\n path,\n body,\n { accessKey: config.accessKey, secretKey: config.secretKey },\n );\n Object.assign(headers, signHeaders);\n\n // 附加玩家 token\n if (token) headers['Authorization'] = `Bearer ${token}`;\n\n const res = await request<T>({\n url: `${apiUrl}${path}`,\n method,\n headers,\n body,\n });\n\n if (res.status >= 400) {\n const msg = (res.data as any)?.message || `HTTP ${res.status}`;\n throw new GameSdkError(msg, res.status, res.data);\n }\n return res.data;\n }\n\n // ── 方法面 ──────────────────────────────────────────\n const sdk: GameSdk = {\n auth: {\n async login(opts: LoginOptions): Promise<LoginResult> {\n const result = await call<LoginResult>('POST', '/auth/login', opts);\n setToken(result.token);\n return result;\n },\n me: () => call<PlayerProfile>('GET', '/auth/me'),\n logout: () => setToken(null),\n getToken,\n },\n\n score: {\n submit: (opts: SubmitScoreOptions) => call<SubmitScoreResult>('POST', '/score/submit', opts),\n },\n\n leaderboard: {\n top: (limit?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard?limit=${limit || 100}`),\n myRank: () => call<MyRank>('GET', '/leaderboard/me'),\n nearby: (range?: number) =>\n call<LeaderboardEntry[]>('GET', `/leaderboard/nearby?range=${range || 5}`),\n },\n\n player: {\n getProfile: () => call<PlayerProfile>('GET', '/player/profile'),\n updateProfile: (patch) => call<PlayerProfile>('PATCH', '/player/profile', patch),\n },\n\n save: {\n set: (key, value) => call('POST', `/storage/${key}`, { value }),\n get: (key) => call('GET', `/storage/${key}`),\n getAll: () => call('GET', '/storage'),\n remove: (key) => call('DELETE', `/storage/${key}`),\n clear: () => call('DELETE', '/storage'),\n },\n };\n\n return sdk;\n}\n\n// ─── 运行时注入配置解析 ─────────────────────────────────\n\n/**\n * 从 __GAME_CONFIG__ 读取平台构建时注入的 gameId 和 baseUrl。\n *\n * gameId: 游戏实例 ID,只有用户通过平台创建游戏并构建后才会存在。\n * 模板开发阶段(mock 模式)不需要 gameId。\n * baseUrl: API 服务地址,由平台构建时注入,开发者无需手动配置。\n *\n * __GAME_CONFIG__ 由平台构建时通过 esbuild define 注入:\n * define: { '__GAME_CONFIG__': JSON.stringify({ gameId, baseUrl }) }\n */\nfunction resolveInjectedConfig(): { gameId?: string; baseUrl: string } {\n try {\n // eslint-disable-next-line no-undef\n const injected = (typeof __GAME_CONFIG__ !== 'undefined' ? __GAME_CONFIG__ : {}) as {\n gameId?: string;\n baseUrl?: string;\n };\n return {\n gameId: injected.gameId,\n baseUrl: injected.baseUrl ?? '',\n };\n } catch {\n return { gameId: undefined, baseUrl: '' };\n }\n}\n\n// ─── Mock 模式(模板开发不连服务端)──────────────────────\n\nlet mockScore = 0;\nlet mockSaves: Record<string, Record<string, any>> = {};\nlet mockToken: string | null = null;\n\nasync function mockResponse<T>(method: string, path: string, body: any): Promise<T> {\n await new Promise((r) => setTimeout(r, 50)); // 模拟网络延迟\n\n // auth\n if (path === '/auth/login') {\n mockToken = `mock_token_${Date.now()}`;\n const profile: PlayerProfile = {\n openId: 'mock_openid',\n platform: body?.platform || 'guest',\n nickname: body?.nickname || '测试玩家',\n bestScore: mockScore,\n playCount: 0,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n };\n return { token: mockToken, openId: 'mock_openid', isNew: true, profile } as T;\n }\n if (path === '/auth/me') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // score\n if (path === '/score/submit') {\n const prev = mockScore;\n mockScore = Math.max(mockScore, body?.score || 0);\n return {\n submitted: body?.score || 0,\n isHighScore: (body?.score || 0) > prev,\n previousBest: prev,\n bestScore: mockScore,\n rank: 1,\n } as T;\n }\n\n // leaderboard\n if (path.startsWith('/leaderboard?')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n if (path === '/leaderboard/me') {\n return { rank: 1, score: mockScore, totalPlayers: 1, playCount: 1 } as T;\n }\n if (path.startsWith('/leaderboard/nearby')) {\n return [{\n rank: 1,\n openId: 'mock***oid',\n nickname: '测试玩家',\n score: mockScore,\n isSelf: true,\n }] as T;\n }\n\n // player\n if (path === '/player/profile' && method === 'GET') {\n return {\n openId: 'mock_openid',\n platform: 'guest',\n nickname: '测试玩家',\n bestScore: mockScore,\n playCount: 1,\n createdAt: new Date().toISOString(),\n lastSeenAt: new Date().toISOString(),\n } as T;\n }\n\n // storage\n if (path.startsWith('/storage/') && method === 'POST') {\n const key = path.split('/storage/')[1];\n mockSaves[key] = body?.value || {};\n return { success: true, key, value: mockSaves[key] } as T;\n }\n if (path.startsWith('/storage/') && method === 'GET') {\n const key = path.split('/storage/')[1];\n return (mockSaves[key] || null) as T;\n }\n if (path === '/storage' && method === 'GET') {\n return mockSaves as T;\n }\n if (path.startsWith('/storage/') && method === 'DELETE') {\n const key = path.split('/storage/')[1];\n delete mockSaves[key];\n return { success: true, key } as T;\n }\n if (path === '/storage' && method === 'DELETE') {\n const count = Object.keys(mockSaves).length;\n mockSaves = {};\n return { success: true, deleted: count } as T;\n }\n\n return {} as T;\n}\n"],"mappings":"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","names":[]}
|