@yoonion/mimi-seed-mcp 0.19.2 → 0.19.3

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.
@@ -4,7 +4,7 @@ export interface BillingEvidence {
4
4
  module: string;
5
5
  version?: string;
6
6
  expression?: string;
7
- source: 'literal' | 'variable' | 'version_catalog' | 'unresolved';
7
+ source: 'literal' | 'variable' | 'version_catalog' | 'transitive' | 'unresolved';
8
8
  }
9
9
  export interface BillingComplianceResult {
10
10
  projectPath: string;
@@ -33,4 +33,8 @@ export interface BillingComplianceResult {
33
33
  automaticExecution: false;
34
34
  };
35
35
  }
36
+ export declare function billingVersionFromPom(pom: string): {
37
+ module: string;
38
+ version: string;
39
+ } | null;
36
40
  export declare function checkBillingCompliance(projectPath: string, now?: Date): Promise<BillingComplianceResult>;
@@ -1,5 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { fetchWithTimeout } from '../lib/http.js';
3
4
  const BILLING_MODULE = /com\.android\.billingclient:billing(?:-ktx)?/;
4
5
  const LITERAL_DEPENDENCY = /com\.android\.billingclient:billing(?:-ktx)?:([0-9]+(?:\.[0-9A-Za-z_-]+){0,3})/g;
5
6
  const VARIABLE_DEPENDENCY = /com\.android\.billingclient:billing(?:-ktx)?:\$\{?([A-Za-z_][A-Za-z0-9_.-]*)\}?/g;
@@ -41,7 +42,8 @@ async function walk(root, maxDepth = 7) {
41
42
  else if (entry.isFile()
42
43
  && (entry.name === 'build.gradle'
43
44
  || entry.name === 'build.gradle.kts'
44
- || entry.name === 'libs.versions.toml')) {
45
+ || entry.name === 'libs.versions.toml'
46
+ || entry.name === 'package.json')) {
45
47
  result.push(path.join(dir, entry.name));
46
48
  }
47
49
  }
@@ -114,6 +116,127 @@ function majorOf(version) {
114
116
  const major = Number.parseInt(version.split('.')[0], 10);
115
117
  return Number.isFinite(major) ? major : null;
116
118
  }
119
+ export function billingVersionFromPom(pom) {
120
+ for (const match of pom.matchAll(/<dependency>([\s\S]*?)<\/dependency>/g)) {
121
+ const block = match[1];
122
+ const group = block.match(/<groupId>\s*([^<]+)\s*<\/groupId>/)?.[1]?.trim();
123
+ const artifact = block.match(/<artifactId>\s*([^<]+)\s*<\/artifactId>/)?.[1]?.trim();
124
+ const version = block.match(/<version>\s*([^<]+)\s*<\/version>/)?.[1]?.trim();
125
+ if (group === 'com.android.billingclient' && /^billing(?:-ktx)?$/.test(artifact ?? '') && version) {
126
+ return { module: `${group}:${artifact}`, version };
127
+ }
128
+ }
129
+ return null;
130
+ }
131
+ async function nearestNodePackage(packageDir, root) {
132
+ let current = packageDir;
133
+ while (isWithin(root, current)) {
134
+ const candidate = path.join(current, 'node_modules', 'react-native-iap');
135
+ try {
136
+ if ((await fs.stat(candidate)).isDirectory())
137
+ return candidate;
138
+ }
139
+ catch {
140
+ // Keep walking toward the project root; workspaces commonly hoist node_modules.
141
+ }
142
+ if (current === root)
143
+ break;
144
+ current = path.dirname(current);
145
+ }
146
+ return null;
147
+ }
148
+ async function reactNativeIapEvidence(root, manifestFile, manifestText) {
149
+ let manifest;
150
+ try {
151
+ manifest = JSON.parse(manifestText);
152
+ }
153
+ catch {
154
+ return null;
155
+ }
156
+ const dependencyGroups = ['dependencies', 'devDependencies', 'optionalDependencies']
157
+ .map((key) => manifest[key])
158
+ .filter((value) => Boolean(value) && typeof value === 'object');
159
+ const declaredVersion = dependencyGroups
160
+ .map((group) => group['react-native-iap'])
161
+ .find((value) => typeof value === 'string');
162
+ if (!declaredVersion)
163
+ return null;
164
+ const relativeManifest = path.relative(root, manifestFile).replace(/\\/g, '/');
165
+ const installedDir = await nearestNodePackage(path.dirname(manifestFile), root);
166
+ if (!installedDir) {
167
+ return {
168
+ file: relativeManifest,
169
+ module: 'com.android.billingclient:billing',
170
+ expression: `react-native-iap ${declaredVersion} is declared but not installed; transitive Billing version is unresolved`,
171
+ source: 'unresolved',
172
+ };
173
+ }
174
+ const directCandidates = [
175
+ path.join(installedDir, 'android', 'build.gradle'),
176
+ path.join(installedDir, 'android', 'build.gradle.kts'),
177
+ ];
178
+ for (const candidate of directCandidates) {
179
+ try {
180
+ const text = await fs.readFile(candidate, 'utf8');
181
+ const direct = [...text.matchAll(LITERAL_DEPENDENCY)][0];
182
+ if (direct) {
183
+ return {
184
+ file: relativeManifest,
185
+ module: direct[0].slice(0, direct[0].lastIndexOf(':')),
186
+ version: direct[1],
187
+ expression: `react-native-iap ${declaredVersion} native dependency`,
188
+ source: 'transitive',
189
+ };
190
+ }
191
+ }
192
+ catch {
193
+ // Newer react-native-iap versions delegate Billing to the OpenIAP Maven artifact.
194
+ }
195
+ }
196
+ let openIapVersion;
197
+ try {
198
+ const versions = JSON.parse(await fs.readFile(path.join(installedDir, 'openiap-versions.json'), 'utf8'));
199
+ if (typeof versions.google === 'string')
200
+ openIapVersion = versions.google;
201
+ }
202
+ catch {
203
+ // Older releases may not use OpenIAP; fall through to an unresolved, safe result.
204
+ }
205
+ if (!openIapVersion || !/^[0-9A-Za-z][0-9A-Za-z._-]*$/.test(openIapVersion)) {
206
+ return {
207
+ file: relativeManifest,
208
+ module: 'com.android.billingclient:billing',
209
+ expression: `react-native-iap ${declaredVersion} detected; transitive Billing version is unresolved`,
210
+ source: 'unresolved',
211
+ };
212
+ }
213
+ const coordinate = `io.github.hyochan.openiap:openiap-google:${openIapVersion}`;
214
+ try {
215
+ const pomUrl = `https://repo.maven.apache.org/maven2/io/github/hyochan/openiap/openiap-google/${openIapVersion}/openiap-google-${openIapVersion}.pom`;
216
+ const response = await fetchWithTimeout(pomUrl, {}, { timeoutMs: 15_000, maxAttempts: 2 });
217
+ if (response.ok) {
218
+ const resolved = billingVersionFromPom(await response.text());
219
+ if (resolved) {
220
+ return {
221
+ file: relativeManifest,
222
+ module: resolved.module,
223
+ version: resolved.version,
224
+ expression: `react-native-iap ${declaredVersion} -> ${coordinate}`,
225
+ source: 'transitive',
226
+ };
227
+ }
228
+ }
229
+ }
230
+ catch {
231
+ // Network failure must not turn a known IAP dependency into not_used.
232
+ }
233
+ return {
234
+ file: relativeManifest,
235
+ module: 'com.android.billingclient:billing',
236
+ expression: `react-native-iap ${declaredVersion} -> ${coordinate}; Maven Billing version lookup failed`,
237
+ source: 'unresolved',
238
+ };
239
+ }
117
240
  function policyAt(now) {
118
241
  const deadlineEnd = (date) => new Date(`${date}T23:59:59.999Z`);
119
242
  const next = BILLING_SUPPORT_SCHEDULE.find((row) => now <= deadlineEnd(row.submissionDeadline));
@@ -175,9 +298,16 @@ export async function checkBillingCompliance(projectPath, now = new Date()) {
175
298
  .map(([, variables]) => variables.get(key))
176
299
  .find((value) => value !== undefined);
177
300
  const evidence = [];
301
+ for (const [file, text] of texts) {
302
+ if (path.basename(file) !== 'package.json')
303
+ continue;
304
+ const transitive = await reactNativeIapEvidence(root, file, text);
305
+ if (transitive && !evidence.some((row) => row.expression === transitive.expression))
306
+ evidence.push(transitive);
307
+ }
178
308
  for (const [file, text] of texts) {
179
309
  const relative = path.relative(root, file).replace(/\\/g, '/');
180
- if (path.basename(file) === 'libs.versions.toml') {
310
+ if (path.basename(file) === 'libs.versions.toml' || path.basename(file) === 'package.json') {
181
311
  continue;
182
312
  }
183
313
  const catalog = catalogFor(file);
@@ -276,7 +406,7 @@ export async function checkBillingCompliance(projectPath, now = new Date()) {
276
406
  else if (unresolved || majors.length === 0) {
277
407
  status = 'unresolved';
278
408
  summary = 'A Billing dependency was found, but at least one version expression could not be resolved statically.';
279
- actions.push('Resolve the reported Gradle variable or version catalog alias and run the check again.');
409
+ actions.push('Resolve the reported Gradle/version catalog expression or install the declared IAP package, then run the check again.');
280
410
  }
281
411
  else if (majors.some((major) => major === policy.minimumSupportedMajor)) {
282
412
  status = 'warning';
@@ -9,7 +9,7 @@ import { checkBillingCompliance } from '../checks/billing.js';
9
9
  export function registerChecksTools(server) {
10
10
  server.tool('android_check_billing_compliance', [
11
11
  '로컬 Android 저장소의 Google Play Billing Library 버전을 읽기 전용으로 탐지하고 제출 정책 마감과 비교합니다.',
12
- 'build.gradle, build.gradle.kts, gradle/libs.versions.toml의 literal·변수·version catalog 검사합니다.',
12
+ 'build.gradle, build.gradle.kts, gradle/libs.versions.toml의 literal·변수·version catalog react-native-iap의 OpenIAP 전이 의존성을 검사합니다.',
13
13
  '코드를 자동 수정하지 않고 공식 Android CLI Skill 설치 명령과 업그레이드 프롬프트만 반환합니다.',
14
14
  ].join(' '), {
15
15
  projectPath: z.string().optional().describe('검사할 프로젝트 절대경로 (기본: MCP 프로세스 현재 디렉터리)'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
4
4
  "description": "Mimi Seed MCP server \u2014 Firebase + AdMob + Google Play + App Store management for Claude Code / Codex / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {