@leonxin/meetgames 0.1.8 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. package/.agents/skills/meet-sdk-regression/SKILL.md +93 -0
  2. package/.cursor/mcp.example.json +16 -0
  3. package/.cursor/mcp.json +11 -0
  4. package/.cursor/skills/meetgames-mcp/SKILL.md +48 -0
  5. package/.vite/vitest/results.json +1 -0
  6. package/README.md +36 -13
  7. package/{meetsdk-android.json → config/meetsdk-android.json} +2 -1
  8. package/config/meetsdk-ios.json +15 -0
  9. package/dist/android/adapter.d.ts.map +1 -1
  10. package/dist/android/adapter.js +2 -2
  11. package/dist/android/adapter.js.map +1 -1
  12. package/dist/android/detect.d.ts +2 -2
  13. package/dist/android/detect.d.ts.map +1 -1
  14. package/dist/android/detect.js +36 -8
  15. package/dist/android/detect.js.map +1 -1
  16. package/dist/cache.d.ts +44 -0
  17. package/dist/cache.d.ts.map +1 -0
  18. package/dist/cache.js +101 -0
  19. package/dist/cache.js.map +1 -0
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +181 -49
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config/meetSdkDefaultConfig.d.ts +19 -2
  24. package/dist/config/meetSdkDefaultConfig.d.ts.map +1 -1
  25. package/dist/config/meetSdkDefaultConfig.js +69 -6
  26. package/dist/config/meetSdkDefaultConfig.js.map +1 -1
  27. package/dist/config/meetSdkIosConfig.d.ts +21 -0
  28. package/dist/config/meetSdkIosConfig.d.ts.map +1 -0
  29. package/dist/config/meetSdkIosConfig.js +68 -0
  30. package/dist/config/meetSdkIosConfig.js.map +1 -0
  31. package/dist/config/meetSdkRemoteConfig.d.ts +19 -1
  32. package/dist/config/meetSdkRemoteConfig.d.ts.map +1 -1
  33. package/dist/config/meetSdkRemoteConfig.js +64 -25
  34. package/dist/config/meetSdkRemoteConfig.js.map +1 -1
  35. package/dist/contracts/types.d.ts +27 -6
  36. package/dist/contracts/types.d.ts.map +1 -1
  37. package/dist/core/doctor.d.ts +17 -0
  38. package/dist/core/doctor.d.ts.map +1 -0
  39. package/dist/core/doctor.js +444 -0
  40. package/dist/core/doctor.js.map +1 -0
  41. package/dist/core/pipeline.d.ts.map +1 -1
  42. package/dist/core/pipeline.js +0 -15
  43. package/dist/core/pipeline.js.map +1 -1
  44. package/dist/core/platform.d.ts +12 -0
  45. package/dist/core/platform.d.ts.map +1 -0
  46. package/dist/core/platform.js +40 -0
  47. package/dist/core/platform.js.map +1 -0
  48. package/dist/core/previewPatches.d.ts +1 -1
  49. package/dist/core/previewPatches.js +2 -2
  50. package/dist/core/previewPatches.js.map +1 -1
  51. package/dist/core/reporter.js +1 -1
  52. package/dist/core/reporter.js.map +1 -1
  53. package/dist/core/workspace.d.ts +2 -2
  54. package/dist/core/workspace.d.ts.map +1 -1
  55. package/dist/core/workspace.js +6 -5
  56. package/dist/core/workspace.js.map +1 -1
  57. package/dist/index.d.ts +4 -1
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +4 -1
  60. package/dist/index.js.map +1 -1
  61. package/dist/ios/channelConfig.d.ts +1 -0
  62. package/dist/ios/channelConfig.d.ts.map +1 -1
  63. package/dist/ios/channelConfig.js +82 -0
  64. package/dist/ios/channelConfig.js.map +1 -1
  65. package/dist/ios/codeUtils.d.ts +1 -0
  66. package/dist/ios/codeUtils.d.ts.map +1 -1
  67. package/dist/ios/codeUtils.js +11 -2
  68. package/dist/ios/codeUtils.js.map +1 -1
  69. package/dist/ios/detect.d.ts +2 -2
  70. package/dist/ios/detect.d.ts.map +1 -1
  71. package/dist/ios/detect.js +49 -10
  72. package/dist/ios/detect.js.map +1 -1
  73. package/dist/ios/entitlements.d.ts +4 -0
  74. package/dist/ios/entitlements.d.ts.map +1 -0
  75. package/dist/ios/entitlements.js +53 -0
  76. package/dist/ios/entitlements.js.map +1 -0
  77. package/dist/ios/fileManager.d.ts.map +1 -1
  78. package/dist/ios/fileManager.js +3 -2
  79. package/dist/ios/fileManager.js.map +1 -1
  80. package/dist/ios/infoPlist.d.ts +1 -1
  81. package/dist/ios/infoPlist.d.ts.map +1 -1
  82. package/dist/ios/infoPlist.js.map +1 -1
  83. package/dist/ios/integrate.d.ts.map +1 -1
  84. package/dist/ios/integrate.js +214 -39
  85. package/dist/ios/integrate.js.map +1 -1
  86. package/dist/ios/pbxprojEditor.d.ts +2 -0
  87. package/dist/ios/pbxprojEditor.d.ts.map +1 -1
  88. package/dist/ios/pbxprojEditor.js +250 -6
  89. package/dist/ios/pbxprojEditor.js.map +1 -1
  90. package/dist/ios/pluginConfig.d.ts +1 -0
  91. package/dist/ios/pluginConfig.d.ts.map +1 -1
  92. package/dist/ios/pluginConfig.js +36 -4
  93. package/dist/ios/pluginConfig.js.map +1 -1
  94. package/dist/ios/sdkBundle.d.ts +1 -6
  95. package/dist/ios/sdkBundle.d.ts.map +1 -1
  96. package/dist/ios/sdkBundle.js +47 -17
  97. package/dist/ios/sdkBundle.js.map +1 -1
  98. package/dist/ios/template.d.ts +1 -0
  99. package/dist/ios/template.d.ts.map +1 -1
  100. package/dist/ios/template.js +14 -1
  101. package/dist/ios/template.js.map +1 -1
  102. package/dist/ios/types.d.ts +2 -2
  103. package/dist/ios/types.d.ts.map +1 -1
  104. package/dist/mcp/server.d.ts.map +1 -1
  105. package/dist/mcp/server.js +22 -15
  106. package/dist/mcp/server.js.map +1 -1
  107. package/dist/mcp/service.d.ts +11 -6
  108. package/dist/mcp/service.d.ts.map +1 -1
  109. package/dist/mcp/service.js +61 -18
  110. package/dist/mcp/service.js.map +1 -1
  111. package/dist/ops/handlers.d.ts.map +1 -1
  112. package/dist/ops/handlers.js +34 -23
  113. package/dist/ops/handlers.js.map +1 -1
  114. package/dist/remote/sdkHomeDownload.d.ts +65 -0
  115. package/dist/remote/sdkHomeDownload.d.ts.map +1 -0
  116. package/dist/remote/sdkHomeDownload.js +229 -0
  117. package/dist/remote/sdkHomeDownload.js.map +1 -0
  118. package/dist/remote/topsdkDownloadSdkConfig.d.ts.map +1 -1
  119. package/dist/remote/topsdkDownloadSdkConfig.js +11 -1
  120. package/dist/remote/topsdkDownloadSdkConfig.js.map +1 -1
  121. package/dist/shared/errors.d.ts +7 -0
  122. package/dist/shared/errors.d.ts.map +1 -0
  123. package/dist/shared/errors.js +16 -0
  124. package/dist/shared/errors.js.map +1 -0
  125. package/docs/API.md +246 -32
  126. package/docs/CLI.md +291 -0
  127. package/docs/INTEGRATION.md +116 -0
  128. package/docs/MCP.md +86 -0
  129. package/docs/README.md +18 -10
  130. package/docs/{api → archive/api}/downloadSDKConfig.md +9 -7
  131. package/docs/{api → archive/api}/getChannelConfig-meetgames.md +2 -2
  132. package/docs/archive/ios-migration.md +2139 -0
  133. package/docs/{ → archive/product/}/346/212/200/346/234/257/346/226/271/346/241/210/350/260/203/347/240/224.md +7 -7
  134. package/docs/{ → archive/product/}/351/234/200/346/261/202/346/226/207/346/241/243.md +16 -15
  135. package/package.json +7 -36
  136. package/recipes/android-default.yaml +0 -5
  137. package/recipes/integrate-default.yaml +0 -5
  138. package/src/android/adapter.ts +9 -0
  139. package/src/android/assembleIntegrationJson.ts +33 -0
  140. package/src/android/detect.ts +132 -0
  141. package/src/android/downloadGoogleServicesJson.ts +56 -0
  142. package/src/android/gradle.ts +116 -0
  143. package/src/android/manifest.ts +50 -0
  144. package/src/android/meetSdkRemoteGradle.ts +837 -0
  145. package/src/cache.ts +164 -0
  146. package/src/cli.ts +496 -0
  147. package/src/config/fetchConfigWrite.ts +30 -0
  148. package/src/config/loadAndroidIntegration.ts +41 -0
  149. package/src/config/loadManifest.ts +40 -0
  150. package/src/config/meetSdkDefaultConfig.ts +100 -0
  151. package/src/config/meetSdkIosConfig.ts +89 -0
  152. package/src/config/meetSdkRemoteConfig.ts +1215 -0
  153. package/src/config/topsdkFeatureModules.ts +92 -0
  154. package/src/contracts/types.ts +129 -0
  155. package/src/core/doctor.ts +485 -0
  156. package/src/core/patch.ts +64 -0
  157. package/src/core/pipeline.ts +107 -0
  158. package/src/core/platform.ts +47 -0
  159. package/src/core/previewPatches.ts +23 -0
  160. package/src/core/reporter.ts +24 -0
  161. package/src/core/workspace.ts +31 -0
  162. package/src/entry.ts +7 -0
  163. package/src/index.ts +140 -0
  164. package/src/ios/channelConfig.ts +128 -0
  165. package/src/ios/codeUtils.ts +160 -0
  166. package/src/ios/detect.ts +105 -0
  167. package/src/ios/entitlements.ts +61 -0
  168. package/src/ios/fileManager.ts +48 -0
  169. package/src/ios/infoPlist.ts +55 -0
  170. package/src/ios/integrate.ts +517 -0
  171. package/src/ios/pbxprojEditor.ts +450 -0
  172. package/src/ios/pluginConfig.ts +97 -0
  173. package/src/ios/reserved.ts +8 -0
  174. package/src/ios/sdkBundle.ts +59 -0
  175. package/src/ios/template.ts +36 -0
  176. package/src/ios/types.ts +65 -0
  177. package/src/mcp/server.ts +176 -0
  178. package/src/mcp/service.ts +253 -0
  179. package/src/mcp-entry.ts +7 -0
  180. package/src/ops/fileStore.ts +56 -0
  181. package/src/ops/handlers.ts +308 -0
  182. package/src/remote/fetchJson.ts +22 -0
  183. package/src/remote/sdkHomeDownload.ts +295 -0
  184. package/src/remote/topsdkDownloadSdkConfig.ts +93 -0
  185. package/src/remote/topsdkGetSdkConfig.ts +122 -0
  186. package/src/remote/topsdkSign.ts +10 -0
  187. package/src/shared/errors.ts +16 -0
  188. package/tests/assemble.test.ts +12 -0
  189. package/tests/doctor.test.ts +91 -0
  190. package/tests/downloadGoogleServicesJson.test.ts +47 -0
  191. package/tests/fetch-remote.test.ts +23 -0
  192. package/tests/fetchConfigOverrides.test.ts +28 -0
  193. package/tests/fetchConfigWrite.test.ts +54 -0
  194. package/tests/fixtures-hosts.test.ts +78 -0
  195. package/tests/gradle.test.ts +33 -0
  196. package/tests/integration-json.test.ts +29 -0
  197. package/tests/ios.codeUtils.test.ts +23 -0
  198. package/tests/ios.sdkBundle.test.ts +21 -0
  199. package/tests/loadManifest.test.ts +15 -0
  200. package/tests/manifest-xml.test.ts +30 -0
  201. package/tests/mcp.e2e.ts +214 -0
  202. package/tests/mcp.service.test.ts +53 -0
  203. package/tests/meetSdkRemoteConfig.test.ts +481 -0
  204. package/tests/meetSdkRemoteGradle.test.ts +414 -0
  205. package/tests/pipeline.android.test.ts +95 -0
  206. package/tests/pipeline.integration-json.test.ts +58 -0
  207. package/tests/pipeline.ios.test.ts +392 -0
  208. package/tests/pipeline.preview.patch.test.ts +85 -0
  209. package/tests/platformSelection.test.ts +77 -0
  210. package/tests/sdkHomeDownload.test.ts +124 -0
  211. package/tests/sdkVersionConfig.test.ts +131 -0
  212. package/tests/topsdk.test.ts +53 -0
  213. package/tests/topsdkDownloadSdkConfig.test.ts +81 -0
  214. package/tests/topsdkFeatureModules.test.ts +116 -0
  215. package/tsconfig.json +19 -0
  216. package/vitest.config.ts +9 -0
  217. package/vitest.mcp.config.ts +11 -0
  218. package/bundled/android/sample.txt +0 -1
  219. package/docs/ANDROID.md +0 -133
  220. package/docs/CURSOR-MCP-SETUP.md +0 -72
  221. package/docs/MCP-SKILL.md +0 -63
  222. package/fixtures/api-samples/getChannelConfig-meetgames.sample.json +0 -123
  223. package/fixtures/meetsdk-remote-config.download-shape.json +0 -20
  224. package/fixtures/meetsdk-remote-config.mock.json +0 -69
  225. package/fixtures/recipes/android-default.fixture.yaml +0 -15
  226. package/fixtures/recipes/android-integration.fixture.json +0 -29
  227. package/fixtures/topsdk-config-reference.json +0 -39
  228. /package/docs/{api → archive/api}/getSDKConfig.md +0 -0
@@ -0,0 +1,93 @@
1
+ import { topsdkMd5Base64Sign } from "./topsdkSign.js";
2
+ import { defaultTopSdkBaseUrl, type TopSdkApiEnv } from "./topsdkGetSdkConfig.js";
3
+
4
+ export { defaultTopSdkBaseUrl, type TopSdkApiEnv };
5
+
6
+ const DOWNLOAD_SDK_CONFIG_PATH = "console/openSDK/downloadSDKConfig";
7
+
8
+ function joinBaseAndPath(baseUrl: string, path: string): URL {
9
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
10
+ return new URL(path, base);
11
+ }
12
+
13
+ /**
14
+ * GET `console/openSDK/downloadSDKConfig` — same sign as `getSDKConfig`:
15
+ * sign = base64(md5(appId + appSecret + timestampMillis)), timestamp within 60s.
16
+ */
17
+ export function buildTopSdkDownloadSdkConfigUrl(params: {
18
+ baseUrl: string;
19
+ appId: string;
20
+ appSecret: string;
21
+ channelType: string;
22
+ timestampMillis?: number;
23
+ }): { url: string; timestampMillis: number } {
24
+ const timestampMillis = params.timestampMillis ?? Date.now();
25
+ const sign = topsdkMd5Base64Sign(params.appId, params.appSecret, timestampMillis);
26
+ const u = joinBaseAndPath(params.baseUrl, DOWNLOAD_SDK_CONFIG_PATH);
27
+ u.searchParams.set("appId", params.appId);
28
+ u.searchParams.set("channelType", params.channelType);
29
+ u.searchParams.set("sign", sign);
30
+ u.searchParams.set("timestamp", String(timestampMillis));
31
+ return { url: u.toString(), timestampMillis };
32
+ }
33
+
34
+ function isRecord(v: unknown): v is Record<string, unknown> {
35
+ return typeof v === "object" && v !== null && !Array.isArray(v);
36
+ }
37
+
38
+ /** gp-sdk error envelope when the handler throws before streaming JSON. */
39
+ export function parseTopSdkApiErrorBody(body: unknown): string | null {
40
+ if (!isRecord(body)) return null;
41
+ const code = body.code;
42
+ if (code === undefined || code === null) return null;
43
+ if (Number(code) === 200) return null;
44
+ const message = typeof body.message === "string" ? body.message : typeof body.error === "string" ? body.error : JSON.stringify(body);
45
+ return `TOPSDK API error (code=${code}): ${message}`;
46
+ }
47
+
48
+ export async function fetchTopSdkDownloadSdkConfig(params: {
49
+ baseUrl: string;
50
+ appId: string;
51
+ appSecret: string;
52
+ channelType: string;
53
+ timestampMillis?: number;
54
+ signal?: AbortSignal;
55
+ }): Promise<{ url: string; body: unknown; text: string }> {
56
+ const { url } = buildTopSdkDownloadSdkConfigUrl(params);
57
+ let res: Response;
58
+ try {
59
+ res = await fetch(url, {
60
+ redirect: "follow",
61
+ signal: params.signal,
62
+ headers: { Accept: "application/json" },
63
+ });
64
+ } catch (e) {
65
+ const cause = e instanceof Error && "cause" in e && e.cause instanceof Error ? e.cause.message : null;
66
+ const detail = cause && cause !== (e instanceof Error ? e.message : String(e)) ? ` (${cause})` : "";
67
+ throw new Error(`GET ${url} failed: ${e instanceof Error ? e.message : String(e)}${detail}`);
68
+ }
69
+ if (!res.ok) {
70
+ let detail = "";
71
+ try {
72
+ const text = await res.text();
73
+ if (text.trim()) {
74
+ detail = `: ${text.trim().slice(0, 1000)}`;
75
+ }
76
+ } catch {
77
+ // Keep the original status-only error if the response body cannot be read.
78
+ }
79
+ throw new Error(`GET ${url} failed: HTTP ${res.status}${detail}`);
80
+ }
81
+ const text = await res.text();
82
+ let body: unknown;
83
+ try {
84
+ body = JSON.parse(text) as unknown;
85
+ } catch {
86
+ throw new Error("Remote response is not valid JSON");
87
+ }
88
+ const apiErr = parseTopSdkApiErrorBody(body);
89
+ if (apiErr) {
90
+ throw new Error(apiErr);
91
+ }
92
+ return { url, body, text };
93
+ }
@@ -0,0 +1,122 @@
1
+ import { fetchJsonGet } from "./fetchJson.js";
2
+ import { topsdkMd5Base64Sign } from "./topsdkSign.js";
3
+
4
+ /**
5
+ * Console API host by environment.
6
+ * - `prod`: 正式服(与文档 `api-sdk-gameplus.meetsocial.com` 一致)
7
+ * - `pre`: 预发
8
+ * - `test`: 测试服(开发调试使用)
9
+ */
10
+ export type TopSdkApiEnv = "prod" | "pre" | "test";
11
+
12
+ export function defaultTopSdkBaseUrl(env: TopSdkApiEnv): string {
13
+ if (env === "test") return "https://test-api-sdk-gameplus.meetsocial.com/";
14
+ if (env === "pre") return "https://pre-api-sdk-gameplus.meetsocial.com/";
15
+ return "https://api-sdk-gameplus.meetsocial.com/";
16
+ }
17
+
18
+ const SDK_CONFIG_PATH = "console/openSDK/getSDKConfig";
19
+
20
+ function joinBaseAndPath(baseUrl: string, path: string): URL {
21
+ const base = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
22
+ return new URL(path, base);
23
+ }
24
+
25
+ /**
26
+ * Same query semantics as topsdk-tool-ios `ChannelConfigManager.getSdkConfig` /
27
+ * topsdk-tool `config_utils.getSdkConfig`: GET `console/openSDK/getSDKConfig` with
28
+ * appId, authType (multi), channelType, sign, timestamp.
29
+ */
30
+ export function buildTopSdkGetSdkConfigUrl(params: {
31
+ baseUrl: string;
32
+ appId: string;
33
+ appSecret: string;
34
+ authTypes: readonly string[];
35
+ channelType: string;
36
+ timestampMillis?: number;
37
+ }): { url: string; timestampMillis: number } {
38
+ const timestampMillis = params.timestampMillis ?? Date.now();
39
+ const sign = topsdkMd5Base64Sign(params.appId, params.appSecret, timestampMillis);
40
+ const u = joinBaseAndPath(params.baseUrl, SDK_CONFIG_PATH);
41
+ u.searchParams.set("appId", params.appId);
42
+ for (const t of params.authTypes) {
43
+ u.searchParams.append("authType", t);
44
+ }
45
+ u.searchParams.set("channelType", params.channelType);
46
+ u.searchParams.set("sign", sign);
47
+ u.searchParams.set("timestamp", String(timestampMillis));
48
+ return { url: u.toString(), timestampMillis };
49
+ }
50
+
51
+ /** Strips auth types not sent to the API (same list as topsdk-tool-ios ChannelConfigManager). */
52
+ export function filterTopSdkAuthTypesForRequest(authTypes: readonly string[]): string[] {
53
+ const skip = new Set(["GUEST", "UI", "IAPPAY"]);
54
+ return authTypes.map((t) => t.trim()).filter(Boolean).filter((t) => !skip.has(t.toUpperCase()));
55
+ }
56
+
57
+ /**
58
+ * Upper-case names aligned with gp-sdk `ThirdPlatformType#getType`(不含 `MOCK`)。
59
+ * 当调用方未传任何 `authType` 时用于 `getSDKConfig`:服务端只返回该渠道已配置的子集。
60
+ */
61
+ export const TOPSDK_GET_SDK_CONFIG_DISCOVERY_AUTH_TYPES: readonly string[] = [
62
+ "NORMAL",
63
+ "GUEST",
64
+ "FACEBOOK",
65
+ "GOOGLE",
66
+ "TOKEN",
67
+ "SNAPCHAT",
68
+ "TWITTER",
69
+ "INSTAGRAM",
70
+ "AMAZON",
71
+ "APPLE",
72
+ "GAMEPLUS",
73
+ "NAVER",
74
+ "KAKAO",
75
+ "LINE",
76
+ "XSOLLA",
77
+ "ONESTORE",
78
+ "SSO",
79
+ "APPSFLYER",
80
+ "ADJUST",
81
+ "FIREBASE",
82
+ "MEETGAMES",
83
+ "EMAIL",
84
+ "HUAWEI",
85
+ "XIAOMI",
86
+ "CATAPPULT",
87
+ "SAMSUNG",
88
+ "TIKTOK",
89
+ ];
90
+
91
+ /** 显式列表为空时,用发现列表再按 `filterTopSdkAuthTypesForRequest` 过滤。 */
92
+ export function resolveAuthTypesForGetSdkConfigRequest(explicit: readonly string[]): string[] {
93
+ const merged = explicit.length > 0 ? explicit : [...TOPSDK_GET_SDK_CONFIG_DISCOVERY_AUTH_TYPES];
94
+ return filterTopSdkAuthTypesForRequest(merged);
95
+ }
96
+
97
+ export async function fetchTopSdkGetSdkConfig(params: {
98
+ baseUrl: string;
99
+ appId: string;
100
+ appSecret: string;
101
+ authTypes: readonly string[];
102
+ channelType: string;
103
+ signal?: AbortSignal;
104
+ }): Promise<{ url: string; timestampMillis: number; body: unknown }> {
105
+ const filtered = resolveAuthTypesForGetSdkConfigRequest(params.authTypes);
106
+ if (filtered.length === 0) {
107
+ return {
108
+ url: joinBaseAndPath(params.baseUrl, SDK_CONFIG_PATH).toString(),
109
+ timestampMillis: Date.now(),
110
+ body: { code: 200, data: { channelAuthConfig: [] }, message: "skipped_no_auth_types_after_filter" },
111
+ };
112
+ }
113
+ const { url, timestampMillis } = buildTopSdkGetSdkConfigUrl({
114
+ baseUrl: params.baseUrl,
115
+ appId: params.appId,
116
+ appSecret: params.appSecret,
117
+ authTypes: filtered,
118
+ channelType: params.channelType,
119
+ });
120
+ const body = await fetchJsonGet(url, { signal: params.signal });
121
+ return { url, timestampMillis, body };
122
+ }
@@ -0,0 +1,10 @@
1
+ import crypto from "node:crypto";
2
+
3
+ /**
4
+ * Same algorithm as topsdk-tool-ios `TOPSDKUtils/ChannelConfigManager.md5_base64_byte`:
5
+ * base64( md5_binary( appId + appSecret + timestampMillis ) )
6
+ */
7
+ export function topsdkMd5Base64Sign(appId: string, appSecret: string, timestampMillis: number): string {
8
+ const plain = `${appId}${appSecret}${String(timestampMillis)}`;
9
+ return crypto.createHash("md5").update(plain, "utf8").digest("base64");
10
+ }
@@ -0,0 +1,16 @@
1
+ export class UserFacingError extends Error {
2
+ readonly code: string;
3
+ readonly details?: string;
4
+
5
+ constructor(code: string, message: string, details?: string) {
6
+ super(message);
7
+ this.name = "UserFacingError";
8
+ this.code = code;
9
+ this.details = details;
10
+ }
11
+ }
12
+
13
+ export function errorMessage(error: unknown): string {
14
+ if (error instanceof Error) return error.message;
15
+ return String(error);
16
+ }
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { unwrapRemoteIntegrationPayload } from "../src/android/assembleIntegrationJson.js";
3
+
4
+ describe("assembleIntegrationJson", () => {
5
+ it("unwraps common envelopes", () => {
6
+ const inner = { version: 1, steps: [{ op: "gradle.insertRepositories", args: { urls: ["mavenCentral"] } }] };
7
+ expect(unwrapRemoteIntegrationPayload(inner)).toBe(inner);
8
+ expect(unwrapRemoteIntegrationPayload({ data: inner })).toEqual(inner);
9
+ expect(unwrapRemoteIntegrationPayload({ config: inner })).toEqual(inner);
10
+ expect(unwrapRemoteIntegrationPayload({ android: inner })).toEqual(inner);
11
+ });
12
+ });
@@ -0,0 +1,91 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { loadManifestFile } from "../src/config/loadManifest.js";
6
+ import { runDoctor } from "../src/core/doctor.js";
7
+ import { detectSinglePlatform, platformContext } from "../src/core/platform.js";
8
+ import { runPipeline } from "../src/core/pipeline.js";
9
+ import { buildWorkspaceContext } from "../src/core/workspace.js";
10
+ import { resolveIosSdkRootFromDirectory } from "../src/remote/sdkHomeDownload.js";
11
+
12
+ const pkgRoot = path.resolve(__dirname, "..");
13
+ const androidLatestRoot = path.join(pkgRoot, "fixtures", "android-test-project", "android-latest-project");
14
+ const iosProjectRoot = path.join(pkgRoot, "fixtures", "ios-test-project", "tooltest");
15
+ const iosRemoteConfigFixture = path.join(pkgRoot, "fixtures", "meetsdk-remote-config.ios-tooltest.json");
16
+ const hasIosProjectFixture = fs.existsSync(iosProjectRoot);
17
+ const fixtureIosSdkRoot = resolveIosSdkRootFromDirectory(path.join(pkgRoot, "fixtures", "ios-sdk", "topSDK-ios--V1.6.0.5"));
18
+
19
+ function copyIosProjectToTemp(): string {
20
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "meet-ios-doctor-"));
21
+ fs.cpSync(iosProjectRoot, tmp, { recursive: true });
22
+ fs.copyFileSync(iosRemoteConfigFixture, path.join(tmp, "meetsdk-remote-config.json"));
23
+ return tmp;
24
+ }
25
+
26
+ function stubSdkHomeVersion(): void {
27
+ vi.stubGlobal(
28
+ "fetch",
29
+ vi.fn(async () => ({
30
+ ok: true,
31
+ status: 200,
32
+ text: async () =>
33
+ JSON.stringify({
34
+ code: 200,
35
+ data: {
36
+ result: {
37
+ android: { ver: "1.6.1.3", date: "2026-05-25" },
38
+ ios: { ver: "1.6.0.5", date: "2026-06-09" },
39
+ },
40
+ },
41
+ }),
42
+ })) as unknown as typeof fetch
43
+ );
44
+ }
45
+
46
+ describe("doctor", () => {
47
+ beforeEach(() => {
48
+ stubSdkHomeVersion();
49
+ });
50
+
51
+ afterEach(() => {
52
+ vi.unstubAllGlobals();
53
+ });
54
+
55
+ it("checks Android integration details from project-root config", async () => {
56
+ const ctx = buildWorkspaceContext(androidLatestRoot, pkgRoot);
57
+ const detected = detectSinglePlatform(ctx);
58
+ expect(detected.ok && detected.platform).toBe("android");
59
+
60
+ const report = await runDoctor(platformContext(ctx, "android"), "android");
61
+
62
+ expect(report.platform).toBe("android");
63
+ expect(report.checks.map((c) => c.name)).toContain("android.repositories");
64
+ expect(report.checks.map((c) => c.name)).toContain("android.dependencies");
65
+ expect(report.checks.map((c) => c.name)).toContain("android.resValues");
66
+ });
67
+
68
+ it.skipIf(!hasIosProjectFixture)("checks iOS integration details from project-root config", async () => {
69
+ const tmp = copyIosProjectToTemp();
70
+ try {
71
+ const manifest = loadManifestFile(path.join(pkgRoot, "recipes", "ios-default.yaml"));
72
+ const applyCtx = buildWorkspaceContext(tmp, pkgRoot, { iosSdkRoot: fixtureIosSdkRoot });
73
+ const { report: applyReport } = await runPipeline(applyCtx, manifest, { dryRun: false });
74
+ expect(applyReport.errors).toEqual([]);
75
+
76
+ const ctx = buildWorkspaceContext(tmp, pkgRoot, { iosSdkRoot: fixtureIosSdkRoot });
77
+ const detected = detectSinglePlatform(ctx);
78
+ expect(detected.ok && detected.platform).toBe("ios");
79
+
80
+ const report = await runDoctor(platformContext(ctx, "ios"), "ios");
81
+
82
+ expect(report.platform).toBe("ios");
83
+ expect(report.checks.map((c) => c.name)).toContain("ios.frameworkFiles");
84
+ expect(report.checks.map((c) => c.name)).toContain("ios.urlSchemes.present");
85
+ expect(report.checks.map((c) => c.name)).toContain("ios.appDelegateInjection");
86
+ expect(report.errors).toEqual([]);
87
+ } finally {
88
+ fs.rmSync(tmp, { recursive: true, force: true });
89
+ }
90
+ });
91
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { resolveGoogleServicesJsonTarget } from "../src/android/downloadGoogleServicesJson.js";
3
+
4
+ describe("resolveGoogleServicesJsonTarget", () => {
5
+ const root = "/proj";
6
+
7
+ it("places file under detected application module with configured name", () => {
8
+ const abs = resolveGoogleServicesJsonTarget(
9
+ root,
10
+ {
11
+ firebase_file_url: "https://x/y.json",
12
+ firebase_file_name: "google-services.json",
13
+ },
14
+ "app"
15
+ );
16
+ expect(abs).toBe("/proj/app/google-services.json");
17
+ });
18
+
19
+ it("uses non-default module directory", () => {
20
+ const abs = resolveGoogleServicesJsonTarget(
21
+ root,
22
+ { firebase_file_url: "https://x/y.json", firebase_file_name: "google-services.json" },
23
+ "launcher"
24
+ );
25
+ expect(abs).toBe("/proj/launcher/google-services.json");
26
+ });
27
+
28
+ it("preserves upload file name with extension", () => {
29
+ const abs = resolveGoogleServicesJsonTarget(
30
+ root,
31
+ {
32
+ firebase_file_url: "https://x/y.json",
33
+ firebase_file_name: "google-services.json",
34
+ },
35
+ "app"
36
+ );
37
+ expect(abs.endsWith("google-services.json")).toBe(true);
38
+ });
39
+
40
+ it("falls back to project root when application module unknown", () => {
41
+ const abs = resolveGoogleServicesJsonTarget(root, {
42
+ firebase_file_url: "https://x/y.json",
43
+ firebase_file_name: "google-services.json",
44
+ });
45
+ expect(abs).toBe("/proj/google-services.json");
46
+ });
47
+ });
@@ -0,0 +1,23 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { fetchJsonGet } from "../src/remote/fetchJson.js";
3
+
4
+ describe("fetchJsonGet", () => {
5
+ afterEach(() => {
6
+ vi.unstubAllGlobals();
7
+ });
8
+
9
+ it("parses JSON bodies", async () => {
10
+ vi.stubGlobal(
11
+ "fetch",
12
+ vi.fn(async () => ({
13
+ ok: true,
14
+ status: 200,
15
+ headers: { get: () => "application/json" },
16
+ text: async () => JSON.stringify({ hello: "world" }),
17
+ })) as unknown as typeof fetch
18
+ );
19
+
20
+ const body = await fetchJsonGet("https://example.com/cfg.json");
21
+ expect(body).toEqual({ hello: "world" });
22
+ });
23
+ });
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { applyFetchConfigCliOverrides, defaultSdkModules } from "../src/config/meetSdkRemoteConfig.js";
3
+
4
+ describe("applyFetchConfigCliOverrides", () => {
5
+ const base = {
6
+ packageName: "com.from.server",
7
+ channel: "google",
8
+ topsdk: {
9
+ appId: "111",
10
+ appSecret: "server-secret",
11
+ version: "",
12
+ groupId: "",
13
+ repositories: [],
14
+ },
15
+ sdkModules: defaultSdkModules(),
16
+ };
17
+
18
+ it("overrides topsdk appId and root channel only", () => {
19
+ const out = applyFetchConfigCliOverrides(base, {
20
+ appId: "222",
21
+ channelType: "google",
22
+ });
23
+ expect(out.topsdk.appId).toBe("222");
24
+ expect(out.channel).toBe("GOOGLE");
25
+ expect(out.topsdk.appSecret).toBe("server-secret");
26
+ expect(out.packageName).toBe("com.from.server");
27
+ });
28
+ });
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it } from "vitest";
5
+ import {
6
+ validateDownloadSdkConfigForWrite,
7
+ writeDownloadSdkConfigRaw,
8
+ } from "../src/config/fetchConfigWrite.js";
9
+
10
+ describe("fetchConfigWrite", () => {
11
+ const tmpDirs: string[] = [];
12
+
13
+ afterEach(() => {
14
+ for (const dir of tmpDirs) {
15
+ fs.rmSync(dir, { recursive: true, force: true });
16
+ }
17
+ tmpDirs.length = 0;
18
+ });
19
+
20
+ it("writeDownloadSdkConfigRaw preserves response bytes (no JSON re-serialize)", () => {
21
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meet-fetch-"));
22
+ tmpDirs.push(dir);
23
+ const out = path.join(dir, "meetsdk-remote-config.json");
24
+ const raw = `{
25
+ "packageName": "com.example.game",
26
+ "channel": "GOOGLE",
27
+ "devicePlatform": "android",
28
+ "topsdk": { "appId": "1", "appSecret": "sec" },
29
+ "sdkModules": { "login": {}, "payment": {}, "analytics": {} }
30
+ }`;
31
+
32
+ writeDownloadSdkConfigRaw(out, raw);
33
+ expect(fs.readFileSync(out, "utf8")).toBe(raw);
34
+ });
35
+
36
+ it("validateDownloadSdkConfigForWrite accepts valid remote config without meetsdk-android fields", () => {
37
+ const body = {
38
+ packageName: "com.example.game",
39
+ channel: "GOOGLE",
40
+ devicePlatform: "android",
41
+ topsdk: { appId: "1", appSecret: "sec" },
42
+ sdkModules: { login: {}, payment: {}, analytics: {} },
43
+ };
44
+ const result = validateDownloadSdkConfigForWrite(body);
45
+ expect(result?.parsed.packageName).toBe("com.example.game");
46
+ expect(result?.parsed.topsdk.version).toBe("");
47
+ expect((result?.parsed as { dependencies?: unknown }).dependencies).toBeUndefined();
48
+ });
49
+
50
+ it("validateDownloadSdkConfigForWrite rejects invalid documents", () => {
51
+ expect(validateDownloadSdkConfigForWrite(null)).toBeNull();
52
+ expect(validateDownloadSdkConfigForWrite({ topsdk: {} })).toBeNull();
53
+ });
54
+ });
@@ -0,0 +1,78 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { describe, expect, it } from "vitest";
6
+ import { buildWorkspaceContext } from "../src/core/workspace.js";
7
+
8
+ const here = path.dirname(fileURLToPath(import.meta.url));
9
+ const pkgRoot = path.resolve(here, "..");
10
+ const androidFixtureRoot = path.join(pkgRoot, "fixtures", "android-test-project");
11
+
12
+ function fixtureProjectRoot(...names: string[]): string {
13
+ for (const name of names) {
14
+ const candidate = path.join(androidFixtureRoot, name);
15
+ if (fs.existsSync(candidate)) return candidate;
16
+ }
17
+ return path.join(androidFixtureRoot, names[0] ?? "");
18
+ }
19
+
20
+ function createAndroidAppModule(root: string, name: string, applicationId: string): void {
21
+ const moduleDir = path.join(root, name);
22
+ fs.mkdirSync(path.join(moduleDir, "src", "main"), { recursive: true });
23
+ fs.writeFileSync(
24
+ path.join(moduleDir, "build.gradle"),
25
+ [
26
+ "plugins {",
27
+ " id 'com.android.application'",
28
+ "}",
29
+ "android {",
30
+ " defaultConfig {",
31
+ ` applicationId '${applicationId}'`,
32
+ " }",
33
+ "}",
34
+ "",
35
+ ].join("\n"),
36
+ "utf8"
37
+ );
38
+ fs.writeFileSync(path.join(moduleDir, "src", "main", "AndroidManifest.xml"), "<manifest />\n", "utf8");
39
+ }
40
+
41
+ describe("fixture host trees", () => {
42
+ const powerRaidRoot = fixtureProjectRoot("power-raid");
43
+
44
+ it("android-latest-project (plugins DSL) resolves application module :app", () => {
45
+ const root = path.join(androidFixtureRoot, "android-latest-project");
46
+ const ctx = buildWorkspaceContext(root, pkgRoot);
47
+ expect(ctx.android?.ok).toBe(true);
48
+ expect(ctx.android?.ok && ctx.android.moduleName).toBe(":app");
49
+ expect(ctx.android?.ok && ctx.android.moduleDir.endsWith(`${path.sep}app`)).toBe(true);
50
+ });
51
+
52
+ it.skipIf(!fs.existsSync(powerRaidRoot))("power-raid resolves launcher application module", () => {
53
+ const ctx = buildWorkspaceContext(powerRaidRoot, pkgRoot);
54
+ expect(ctx.android?.ok).toBe(true);
55
+ expect(ctx.android?.ok && ctx.android.moduleName).toBe(":launcher");
56
+ expect(ctx.android?.ok && ctx.android.moduleDir.endsWith(`${path.sep}launcher`)).toBe(true);
57
+ });
58
+
59
+ it("requires an Android app module selector when multiple application modules exist", () => {
60
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "meet-android-multi-app-"));
61
+ fs.writeFileSync(path.join(root, "settings.gradle"), "include ':app'\ninclude ':demo'\n", "utf8");
62
+ createAndroidAppModule(root, "app", "com.example.app");
63
+ createAndroidAppModule(root, "demo", "com.example.demo");
64
+
65
+ const ambiguous = buildWorkspaceContext(root, pkgRoot);
66
+ expect(ambiguous.android?.ok).toBe(false);
67
+ expect(ambiguous.android?.ok ? "" : ambiguous.android?.error).toContain("MULTIPLE_APPLICATION_MODULES_FOUND: :app, :demo");
68
+
69
+ const selected = buildWorkspaceContext(root, pkgRoot, { appTarget: "demo" });
70
+ expect(selected.android?.ok).toBe(true);
71
+ expect(selected.android?.ok && selected.android.moduleName).toBe(":demo");
72
+ expect(selected.android?.ok && selected.android.moduleDir.endsWith(`${path.sep}demo`)).toBe(true);
73
+
74
+ const missing = buildWorkspaceContext(root, pkgRoot, { appTarget: "missing" });
75
+ expect(missing.android?.ok).toBe(false);
76
+ expect(missing.android?.ok ? "" : missing.android?.error).toContain("ANDROID_APPLICATION_MODULE_NOT_FOUND: :missing");
77
+ });
78
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { updateModuleBuildGradleDependencies, updateRootBuildGradleRepositories } from "../src/android/gradle.js";
3
+
4
+ describe("gradle editors", () => {
5
+ it("inserts managed repositories", () => {
6
+ const root = `
7
+ allprojects {
8
+ repositories {
9
+ google()
10
+ }
11
+ }
12
+ `;
13
+ const res = updateRootBuildGradleRepositories(root, ["https://m.example.com"]);
14
+ expect(res.ok).toBe(true);
15
+ if (!res.ok) return;
16
+ expect(res.content).toContain("MEET_INTEGRATE REPOS");
17
+ expect(res.content).toContain("m.example.com");
18
+ });
19
+
20
+ it("inserts managed dependencies", () => {
21
+ const mod = `
22
+ apply plugin: 'com.android.application'
23
+ android { defaultConfig { } }
24
+ dependencies {
25
+ }
26
+ `;
27
+ const res = updateModuleBuildGradleDependencies(mod, [` implementation 'com.demo:lib:1.0'`]);
28
+ expect(res.ok).toBe(true);
29
+ if (!res.ok) return;
30
+ expect(res.content).toContain("MEET_INTEGRATE BLOCK");
31
+ expect(res.content).toContain("com.demo:lib");
32
+ });
33
+ });
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { loadAndroidIntegrationFile } from "../src/config/loadAndroidIntegration.js";
6
+
7
+ describe("android integration json", () => {
8
+ it("loads and validates integration file", () => {
9
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "meet-int-json-"));
10
+ const p = path.join(dir, "cfg.json");
11
+ fs.writeFileSync(
12
+ p,
13
+ JSON.stringify(
14
+ {
15
+ version: 1,
16
+ meta: { sourceUrl: "https://example.com/cfg", fetchedAt: "2026-01-01T00:00:00.000Z" },
17
+ steps: [{ op: "gradle.insertRepositories", platform: "android", args: { urls: ["mavenCentral"] } }],
18
+ },
19
+ null,
20
+ 2
21
+ ),
22
+ "utf8"
23
+ );
24
+ const doc = loadAndroidIntegrationFile(p);
25
+ expect(doc.version).toBe(1);
26
+ expect(doc.steps.length).toBe(1);
27
+ fs.rmSync(dir, { recursive: true, force: true });
28
+ });
29
+ });
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { CodeUtils } from "../src/ios/codeUtils.js";
3
+
4
+ describe("CodeUtils", () => {
5
+ it("adds header and method body without duplicating", () => {
6
+ const initial = `@implementation AppDelegate
7
+ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
8
+ return YES;
9
+ }
10
+ @end
11
+ `;
12
+ const cu = new CodeUtils("AppDelegate.m", initial);
13
+ cu.addHeader('#import <TOPSDK/TopSDK.h>');
14
+ cu.addCodeToMethod(
15
+ "- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions",
16
+ "[TopSDK.sharedInstance application:application didFinishLaunchingWithOptions:launchOptions]"
17
+ );
18
+ cu.addHeader('#import <TOPSDK/TopSDK.h>');
19
+ expect(cu.text).toContain("#import <TOPSDK/TopSDK.h>");
20
+ expect(cu.text).toContain("TopSDK.sharedInstance");
21
+ expect(cu.text.split("#import <TOPSDK/TopSDK.h>").length - 1).toBe(1);
22
+ });
23
+ });