@yoonion/mimi-seed-mcp 0.13.1 → 0.13.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.
@@ -2,6 +2,7 @@ import { JWT } from 'google-auth-library';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
5
+ import { PLAY_DEVELOPER_REPORTING_SCOPE } from './scopes.js';
5
6
  const CONFIG_DIR = path.join(os.homedir(), '.mimi-seed');
6
7
  const SA_DIR = path.join(CONFIG_DIR, 'play-service-accounts');
7
8
  const LEGACY_SA_PATH = path.join(CONFIG_DIR, 'play-service-account.json');
@@ -112,7 +113,12 @@ export function getServiceAccountClient(packageName) {
112
113
  return new JWT({
113
114
  email: parsed.client_email,
114
115
  key: parsed.private_key,
115
- scopes: ['https://www.googleapis.com/auth/androidpublisher'],
116
+ // androidpublisher(edits/리스팅 등) + Developer Reporting(vitals 통계). 통계 도구가
117
+ // Reporting API 를 쓰므로 이 스코프가 없으면 SA 경로에서도 통계만 403 으로 죽는다.
118
+ scopes: [
119
+ 'https://www.googleapis.com/auth/androidpublisher',
120
+ PLAY_DEVELOPER_REPORTING_SCOPE,
121
+ ],
116
122
  });
117
123
  }
118
124
  catch {
@@ -41,8 +41,8 @@ export declare const AUTH_DOMAINS: {
41
41
  };
42
42
  readonly playstore: {
43
43
  readonly label: "Play Store";
44
- readonly scopes: readonly ["https://www.googleapis.com/auth/androidpublisher"];
45
- readonly summary: "playstore_* — 서비스 계정 없이 OAuth 로 하는 Play Console 작업";
44
+ readonly scopes: readonly ["https://www.googleapis.com/auth/androidpublisher", "https://www.googleapis.com/auth/playdeveloperreporting"];
45
+ readonly summary: "playstore_* — OAuth 로 하는 Play Console 작업 + Android vitals 통계";
46
46
  };
47
47
  readonly googleads: {
48
48
  readonly label: "Google Ads";
@@ -69,6 +69,16 @@ export type AuthDomainId = keyof typeof AUTH_DOMAINS;
69
69
  /** z.enum 등 튜플이 필요한 자리에 쓰는 도메인 id 목록 (선언 순서 유지). */
70
70
  export declare const DOMAIN_IDS: [AuthDomainId, ...AuthDomainId[]];
71
71
  export declare const CLOUD_PLATFORM_SCOPE: "https://www.googleapis.com/auth/cloud-platform";
72
+ /**
73
+ * Play Developer Reporting API(Android vitals 통계) 전용 스코프. androidpublisher 와
74
+ * 별개다 — SA JWT 와 OAuth playstore 도메인 양쪽에 함께 실어야 통계 도구가 동작한다.
75
+ *
76
+ * CLOUD_PLATFORM_SCOPE 처럼 도메인 정의에서 **파생**한다(리터럴을 두 번 적지 않는다).
77
+ * 별도 리터럴로 두면 도메인 배열과 어긋나도 어떤 테스트도 못 잡고, SA(const 사용)와
78
+ * OAuth pre-flight(도메인 리터럴)가 다른 문자열을 봐 정상 로그인이 INSUFFICIENT_SCOPE 로
79
+ * 죽는 함정이 된다. auth-scopes.test.ts 가 값과 도메인 포함 여부를 함께 고정한다.
80
+ */
81
+ export declare const PLAY_DEVELOPER_REPORTING_SCOPE: "https://www.googleapis.com/auth/playdeveloperreporting";
72
82
  /**
73
83
  * 공백 구분 scope 문자열들의 합집합. tokens.json 의 scope 는 누적(monotonic)이어야 하므로
74
84
  * 로그인/갱신 시 기존 기록 + 새 응답을 합쳐 저장하는 데 쓴다. undefined/빈 문자열은 무시.
@@ -37,8 +37,14 @@ export const AUTH_DOMAINS = {
37
37
  },
38
38
  playstore: {
39
39
  label: 'Play Store',
40
- scopes: ['https://www.googleapis.com/auth/androidpublisher'],
41
- summary: 'playstore_* — 서비스 계정 없이 OAuth 로 하는 Play Console 작업',
40
+ scopes: [
41
+ 'https://www.googleapis.com/auth/androidpublisher',
42
+ // Android vitals 통계(playstore_get_statistics)는 Play Developer Reporting API 를
43
+ // 쓰는데 androidpublisher 와 별개 스코프가 필요하다. 빠져 있으면 통계만 런타임
44
+ // 403(ACCESS_TOKEN_SCOPE_INSUFFICIENT)으로 죽고, 다른 Play 도구는 멀쩡해 원인 추적이 어렵다.
45
+ 'https://www.googleapis.com/auth/playdeveloperreporting',
46
+ ],
47
+ summary: 'playstore_* — OAuth 로 하는 Play Console 작업 + Android vitals 통계',
42
48
  },
43
49
  googleads: {
44
50
  label: 'Google Ads',
@@ -70,6 +76,16 @@ export const AUTH_DOMAINS = {
70
76
  /** z.enum 등 튜플이 필요한 자리에 쓰는 도메인 id 목록 (선언 순서 유지). */
71
77
  export const DOMAIN_IDS = Object.keys(AUTH_DOMAINS);
72
78
  export const CLOUD_PLATFORM_SCOPE = AUTH_DOMAINS.gcp.scopes[0];
79
+ /**
80
+ * Play Developer Reporting API(Android vitals 통계) 전용 스코프. androidpublisher 와
81
+ * 별개다 — SA JWT 와 OAuth playstore 도메인 양쪽에 함께 실어야 통계 도구가 동작한다.
82
+ *
83
+ * CLOUD_PLATFORM_SCOPE 처럼 도메인 정의에서 **파생**한다(리터럴을 두 번 적지 않는다).
84
+ * 별도 리터럴로 두면 도메인 배열과 어긋나도 어떤 테스트도 못 잡고, SA(const 사용)와
85
+ * OAuth pre-flight(도메인 리터럴)가 다른 문자열을 봐 정상 로그인이 INSUFFICIENT_SCOPE 로
86
+ * 죽는 함정이 된다. auth-scopes.test.ts 가 값과 도메인 포함 여부를 함께 고정한다.
87
+ */
88
+ export const PLAY_DEVELOPER_REPORTING_SCOPE = AUTH_DOMAINS.playstore.scopes[1];
73
89
  function dedupe(scopes) {
74
90
  return [...new Set(scopes)];
75
91
  }
package/dist/helpers.d.ts CHANGED
@@ -18,6 +18,6 @@ export declare const APPSTORE_AUTH_HINT: string;
18
18
  * 별도 서비스 계정 JSON 을 받지 않아도 대부분의 Play 작업이 가능하다.
19
19
  * (서비스 계정은 서버/헤드리스 — onesub 영수증 검증 등 — 용도로 계속 우선 적용.)
20
20
  */
21
- export declare function requirePlayStoreAuth(packageName?: string): import("google-auth-library").OAuth2Client;
21
+ export declare function requirePlayStoreAuth(packageName?: string, requiredScope?: string): import("google-auth-library").OAuth2Client;
22
22
  export declare function requireServiceAccountJson(packageName?: string): string;
23
23
  export declare function requireAppStoreCreds(): import("./appstore/auth.js").AppStoreCredentials;
package/dist/helpers.js CHANGED
@@ -15,6 +15,39 @@ function formatAuthError(p) {
15
15
  .filter((l) => l !== '')
16
16
  .join('\n');
17
17
  }
18
+ /**
19
+ * 도구가 요구하는 스코프의 pre-flight 검사. expiry 만 보는 ensureFreshAccessToken 은
20
+ * 스코프 미보유를 못 걸러내므로(런타임 ACCESS_TOKEN_SCOPE_INSUFFICIENT), 저장된 scope 로
21
+ * 결정적인 안내를 던진다. 도메인 선택형 로그인 도입 후에는 "전체 재로그인"이 아니라
22
+ * "--domains <해당 도메인> 으로 추가 부여(기존 권한 유지)"가 올바른 해법이다.
23
+ *
24
+ * scope 가 undefined 인 구 토큰(스코프 추적 도입 전 full-scope 로그인)은 추적 도입
25
+ * 이전부터 있던 스코프는 보유한 게 확실하므로 통과시킨다 — 안 그러면 pre-flight 를 새로
26
+ * 다는 순간 멀쩡한 기존 사용자에게 재로그인을 강제한다. 추적 이후 추가된 스코프(GA4,
27
+ * Play Developer Reporting)만 미보유 확정으로 본다.
28
+ *
29
+ * OAuth 토큰에만 적용된다 — SA JWT 는 자체 scopes 로 토큰을 받으므로 이 검사 대상이 아니다.
30
+ */
31
+ function assertStoredScope(requiredScope) {
32
+ if (!requiredScope)
33
+ return;
34
+ const scopeStr = getStoredTokens()?.scope;
35
+ const missing = scopeStr === undefined
36
+ ? !isPreTrackingScope(requiredScope)
37
+ : !scopeStr.split(' ').filter(Boolean).includes(requiredScope);
38
+ if (!missing)
39
+ return;
40
+ const domainArg = domainsForScope(requiredScope).join(',');
41
+ throw new Error(formatAuthError({
42
+ code: 'INSUFFICIENT_SCOPE',
43
+ message: `이 도구는 추가 권한이 필요해 (${requiredScope}). 현재 로그인에 그 권한이 없어.`,
44
+ hint: domainArg
45
+ ? `mimi-seed-auth --domains ${domainArg} 로 재로그인하면 기존 권한은 유지한 채 이 권한만 추가돼.`
46
+ : 'mimi-seed-auth 로 재로그인하면 새 권한이 부여돼.',
47
+ retriable: false,
48
+ needsReauth: true,
49
+ }));
50
+ }
18
51
  /**
19
52
  * OAuth 클라이언트 확보 — 호출 전에 access_token 을 사전 갱신하고,
20
53
  * refresh_token 이 만료/누락이면 raw googleapis 에러(invalid_grant 등) 대신
@@ -38,33 +71,7 @@ export async function requireAuth(requiredScope) {
38
71
  needsReauth: true,
39
72
  }));
40
73
  }
41
- // 도구가 요구하는 스코프의 pre-flight 검사. expiry 만 보는 ensureFreshAccessToken 은
42
- // 스코프 미보유를 못 걸러내므로(런타임 ACCESS_TOKEN_SCOPE_INSUFFICIENT), 저장된 scope 로
43
- // 결정적인 안내를 던진다. 도메인 선택형 로그인 도입 후에는 "전체 재로그인"이 아니라
44
- // "--domains <해당 도메인> 으로 추가 부여(기존 권한 유지)"가 올바른 해법이다.
45
- //
46
- // scope 가 undefined 인 구 토큰(스코프 추적 도입 전 full-scope 로그인)은 추적 도입
47
- // 이전부터 있던 스코프는 보유한 게 확실하므로 통과시킨다 — 안 그러면 pre-flight 를 새로
48
- // 다는 순간 멀쩡한 기존 사용자에게 재로그인을 강제한다. 추적 이후 추가된 스코프(GA4)만
49
- // 미보유 확정으로 본다.
50
- if (requiredScope) {
51
- const scopeStr = getStoredTokens()?.scope;
52
- const missing = scopeStr === undefined
53
- ? !isPreTrackingScope(requiredScope)
54
- : !scopeStr.split(' ').filter(Boolean).includes(requiredScope);
55
- if (missing) {
56
- const domainArg = domainsForScope(requiredScope).join(',');
57
- throw new Error(formatAuthError({
58
- code: 'INSUFFICIENT_SCOPE',
59
- message: `이 도구는 추가 권한이 필요해 (${requiredScope}). 현재 로그인에 그 권한이 없어.`,
60
- hint: domainArg
61
- ? `mimi-seed-auth --domains ${domainArg} 로 재로그인하면 기존 권한은 유지한 채 이 권한만 추가돼.`
62
- : 'mimi-seed-auth 로 재로그인하면 새 권한이 부여돼.',
63
- retriable: false,
64
- needsReauth: true,
65
- }));
66
- }
67
- }
74
+ assertStoredScope(requiredScope);
68
75
  return client;
69
76
  }
70
77
  export const PLAY_AUTH_HINT = [
@@ -95,13 +102,17 @@ export const APPSTORE_AUTH_HINT = [
95
102
  * 별도 서비스 계정 JSON 을 받지 않아도 대부분의 Play 작업이 가능하다.
96
103
  * (서비스 계정은 서버/헤드리스 — onesub 영수증 검증 등 — 용도로 계속 우선 적용.)
97
104
  */
98
- export function requirePlayStoreAuth(packageName) {
105
+ export function requirePlayStoreAuth(packageName, requiredScope) {
99
106
  const sa = getServiceAccountClient(packageName);
100
107
  if (sa)
101
108
  return sa;
102
109
  const oauth = getAuthenticatedClient();
103
- if (oauth)
110
+ if (oauth) {
111
+ // OAuth 폴백일 때만 스코프 pre-flight — SA JWT 는 자체 scopes 로 토큰을 받으므로
112
+ // (getServiceAccountClient 가 reporting 스코프를 이미 싣는다) 해당 없음.
113
+ assertStoredScope(requiredScope);
104
114
  return oauth;
115
+ }
105
116
  throw new Error(PLAY_AUTH_HINT);
106
117
  }
107
118
  export function requireServiceAccountJson(packageName) {
@@ -4,6 +4,7 @@ import { friendlyPlayError } from '../playstore/errors.js';
4
4
  import { saveServiceAccountJsonForPackage, listRegisteredServiceAccounts, deleteServiceAccountJsonForPackage, } from '../auth/playstore-auth.js';
5
5
  import { createGoogleOneTimePurchase, createGoogleSubscription, updateGoogleProduct, deleteGoogleProduct, listGoogleProducts, } from '@onesub/providers';
6
6
  import { requirePlayStoreAuth, requireServiceAccountJson, requireAuth } from '../helpers.js';
7
+ import { PLAY_DEVELOPER_REPORTING_SCOPE } from '../auth/scopes.js';
7
8
  import * as iam from '../iam/tools.js';
8
9
  import { buildPlayStoreReleasePlan } from '../checks/plan.js';
9
10
  import { validatePlayReleaseNotes, formatIssuesForUser } from '../lib/text-validators.js';
@@ -115,7 +116,9 @@ export function registerPlaystoreTools(server) {
115
116
  userCohort: z.enum(['OS_PUBLIC', 'APP_TESTERS', 'OS_BETA']).optional().describe('사용자 cohort'),
116
117
  timeZone: z.string().optional().describe('날짜 기준 timezone. 기본: America/Los_Angeles (Play Reporting API 샘플/지원 기준)'),
117
118
  }, async (args) => {
118
- const auth = requirePlayStoreAuth(args.packageName);
119
+ // 통계는 Play Developer Reporting API 를 쓰므로 OAuth 폴백 시 reporting 스코프를
120
+ // 요구한다. 미보유면 generic 403 대신 "--domains playstore 재로그인" 안내가 나간다.
121
+ const auth = requirePlayStoreAuth(args.packageName, PLAY_DEVELOPER_REPORTING_SCOPE);
119
122
  const result = await playstore.getStatistics(auth, args.packageName, args);
120
123
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
121
124
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.13.1",
3
+ "version": "0.13.3",
4
4
  "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Codex / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {