@yoonion/mimi-seed-mcp 0.17.1 → 0.18.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.
@@ -9,8 +9,39 @@ export interface AppStoreCredentials {
9
9
  * 여기 적어두는 수밖에 없다. 리포트 도구에만 쓰이므로 없어도 나머지는 다 동작한다.
10
10
  */
11
11
  vendorNumber?: string;
12
+ /**
13
+ * 매출 리포트 전용 별도 키 (선택).
14
+ *
15
+ * 리포트 엔드포인트는 다른 ASC API 와 요구 롤이 다르다 —
16
+ * **Admin / Finance / Sales and Reports** 중 하나여야 한다. 배포에 쓰는 키는 보통
17
+ * App Manager 라 여기서만 403 이 나는데, **Apple 은 발급된 키의 롤을 수정할 수 없게**
18
+ * 해놨다(폐기 후 재발급만 가능).
19
+ *
20
+ * 그렇다고 배포 키를 Admin 으로 갈아끼우면 잘 돌던 릴리스 파이프라인의 자격증명을
21
+ * 전부 교체해야 하고, 권한도 사용자·재무까지 넓어진다. 그래서 **읽기 전용 Finance 키를
22
+ * 따로 두고 리포트 도구만 이걸 쓰게** 한다. 배포 키는 손대지 않는다.
23
+ *
24
+ * 없으면 최상위 키로 폴백하므로, 배포 키가 이미 Admin 이면 설정할 필요가 없다.
25
+ */
26
+ reportsKey?: AppStoreKey;
27
+ }
28
+ /** ASC API 키 한 벌. 최상위 자격증명과 reportsKey 가 같은 모양을 공유한다. */
29
+ export interface AppStoreKey {
30
+ issuerId: string;
31
+ keyId: string;
32
+ privateKey: string;
12
33
  }
13
34
  export declare function getAppStoreCredentials(): AppStoreCredentials | null;
14
35
  export declare function saveAppStoreCredentials(creds: AppStoreCredentials): void;
15
36
  export declare function generateToken(creds: AppStoreCredentials): Promise<string>;
16
37
  export declare function getAuthHeaders(): Promise<Record<string, string> | null>;
38
+ /**
39
+ * 매출 리포트용 헤더 — reportsKey 가 있으면 그걸로, 없으면 최상위 키로 폴백.
40
+ *
41
+ * 폴백이 조용하면 안 된다: 403 이 났을 때 "어느 키가 거부당했는지" 를 모르면 사용자가
42
+ * 엉뚱한 키의 롤을 들여다보게 된다. 그래서 어느 키를 썼는지 함께 돌려준다.
43
+ */
44
+ export declare function getReportsAuthHeaders(): Promise<{
45
+ headers: Record<string, string>;
46
+ source: 'reportsKey' | 'default';
47
+ } | null>;
@@ -66,3 +66,24 @@ export async function getAuthHeaders() {
66
66
  const token = await generateToken(creds);
67
67
  return { Authorization: `Bearer ${token}` };
68
68
  }
69
+ /**
70
+ * 매출 리포트용 헤더 — reportsKey 가 있으면 그걸로, 없으면 최상위 키로 폴백.
71
+ *
72
+ * 폴백이 조용하면 안 된다: 403 이 났을 때 "어느 키가 거부당했는지" 를 모르면 사용자가
73
+ * 엉뚱한 키의 롤을 들여다보게 된다. 그래서 어느 키를 썼는지 함께 돌려준다.
74
+ */
75
+ export async function getReportsAuthHeaders() {
76
+ const creds = getAppStoreCredentials();
77
+ if (!creds)
78
+ return null;
79
+ if (creds.reportsKey) {
80
+ return {
81
+ headers: { Authorization: `Bearer ${await generateToken(creds.reportsKey)}` },
82
+ source: 'reportsKey',
83
+ };
84
+ }
85
+ return {
86
+ headers: { Authorization: `Bearer ${await generateToken(creds)}` },
87
+ source: 'default',
88
+ };
89
+ }
@@ -9,9 +9,9 @@
9
9
  // sandbox 가 들어오지 않는 창구를 봐야 하고, 그게 이 리포트다.
10
10
  import zlib from 'node:zlib';
11
11
  import { fetchWithTimeout } from '../lib/http.js';
12
- import { getAppStoreCredentials } from './auth.js';
12
+ import { getAppStoreCredentials, getReportsAuthHeaders } from './auth.js';
13
13
  import { friendlyAppStoreError } from './errors.js';
14
- import { authHeadersOrThrow, V1_BASE } from './http.js';
14
+ import { V1_BASE } from './http.js';
15
15
  /**
16
16
  * vendorNumber 해석: 명시 인자 > ~/.mimi-seed/appstore.json 의 vendorNumber.
17
17
  *
@@ -41,10 +41,18 @@ function resolveVendorNumber(explicit) {
41
41
  * 여기서는 빈 배열로 뭉개지 않고 notFound 플래그를 그대로 올려보낸다.
42
42
  */
43
43
  async function fetchReport(resourcePath, params) {
44
- const authHeaders = await authHeadersOrThrow();
44
+ const auth = await getReportsAuthHeaders();
45
+ if (!auth) {
46
+ throw new Error([
47
+ '❌ App Store Connect 인증이 필요해.',
48
+ '',
49
+ '터미널에서 실행:',
50
+ ' npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
51
+ ].join('\n'));
52
+ }
45
53
  const query = new URLSearchParams(params).toString();
46
54
  const response = await fetchWithTimeout(`${V1_BASE}${resourcePath}?${query}`, {
47
- headers: { ...authHeaders, Accept: 'application/a-gzip' },
55
+ headers: { ...auth.headers, Accept: 'application/a-gzip' },
48
56
  });
49
57
  if (response.status === 404)
50
58
  return { notFound: true, rows: [], raw: '' };
@@ -54,18 +62,21 @@ async function fetchReport(resourcePath, params) {
54
62
  // "키가 깨졌나" 로 헤매기 쉽다.
55
63
  throw new Error([
56
64
  '❌ 매출 리포트 접근 거부 (403) — 키는 정상인데 **롤이 부족**하다.',
65
+ ` 거부당한 키: ${auth.source === 'reportsKey' ? 'reportsKey (리포트 전용)' : '최상위 키 (배포용과 동일)'}`,
57
66
  '',
58
67
  '리포트 엔드포인트는 다른 App Store Connect API 와 요구 롤이 다르다:',
59
68
  ' 필요: **Admin / Finance / Sales and Reports(ACCESS_TO_REPORTS)** 중 하나',
60
69
  'App Manager·Developer 키는 앱 메타데이터는 다 되는데 여기서만 막힌다 —',
61
70
  '다른 도구가 잘 도는 것은 이 403 과 아무 관계가 없다.',
62
71
  '',
63
- 'App Store Connect > 사용자 액세스 > 통합 > App Store Connect API 에서',
64
- '해당 키의 액세스 권한을 확인할 것. 키의 롤은 나중에 못 바꾸는 경우가 있으니,',
65
- '그때는 위 롤로 **새 키를 발급**하고 다시 등록한다:',
66
- ' npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
72
+ '⚠️ **발급된 키의 롤은 수정할 없다** (Apple 정책 폐기 재발급만 가능).',
73
+ '그렇다고 배포 키를 갈아끼우면 릴리스 파이프라인 자격증명을 전부 교체해야 한다.',
67
74
  '',
68
- '⚠️ 키를 바꾸면 배포 파이프라인이 같은 키를 쓰는지도 함께 확인할 것.',
75
+ '권장: **읽기 전용 Finance 키를 따로 발급**해 리포트 도구만 쓰게 한다.',
76
+ ' 1) ASC > 사용자 및 액세스 > 통합 > 팀 키 > 키 생성, 액세스 = Finance',
77
+ ' 2) 받은 .p8 내용을 ~/.mimi-seed/appstore.json 의 reportsKey 에 넣는다:',
78
+ ' "reportsKey": { "issuerId": "...", "keyId": "...", "privateKey": "-----BEGIN..." }',
79
+ '배포 키는 그대로 두면 된다 — 리포트 도구만 reportsKey 를 쓴다.',
69
80
  ].join('\n'));
70
81
  }
71
82
  if (!response.ok) {
@@ -217,14 +228,23 @@ export async function probeReportsAccess() {
217
228
  'filter[reportDate]': probeDate,
218
229
  'filter[version]': '1_0',
219
230
  });
220
- return { status: 'ok', detail: `매출 리포트 접근 가능 (vendorNumber ${vendorNumber})` };
231
+ const usingSeparateKey = getAppStoreCredentials()?.reportsKey != null;
232
+ return {
233
+ status: 'ok',
234
+ detail: `매출 리포트 접근 가능 (vendorNumber ${vendorNumber}, ` +
235
+ `${usingSeparateKey ? 'reportsKey 사용' : '최상위 키 사용'})`,
236
+ };
221
237
  }
222
238
  catch (err) {
223
239
  const message = err?.message ?? '';
224
240
  if (message.includes('403')) {
241
+ const usingSeparateKey = getAppStoreCredentials()?.reportsKey != null;
225
242
  return {
226
243
  status: 'forbidden',
227
- detail: '매출 리포트 403 — 키 롤 부족 (Admin / Finance / Sales and Reports 필요)',
244
+ detail: '매출 리포트 403 — 키 롤 부족 (Admin / Finance / Sales and Reports 필요). ' +
245
+ (usingSeparateKey
246
+ ? 'reportsKey 가 거부당했다.'
247
+ : 'reportsKey 를 따로 두면 배포 키를 건드리지 않아도 된다.'),
228
248
  };
229
249
  }
230
250
  return { status: 'error', detail: message.split('\n')[0] };
@@ -1517,6 +1517,9 @@ export function registerAppstoreTools(server) {
1517
1517
  '⚠️ 데이터가 없는 날짜는 Apple 이 404 를 주므로 datesWithoutData 로 따로 돌려준다 —',
1518
1518
  '"매출 0" 과 "리포트 미생성/설정 오류"를 섞지 말 것. 당일치는 보통 아직 없다.',
1519
1519
  'vendorNumber 는 ~/.mimi-seed/appstore.json 에 저장해두면 생략 가능.',
1520
+ '⚠️ **리포트는 요구 롤이 다르다** — Admin/Finance/Sales and Reports 중 하나여야 하고,',
1521
+ '배포에 흔히 쓰는 App Manager 키는 여기서만 403 이 난다. 발급된 키의 롤은 수정할 수 없으므로,',
1522
+ '읽기 전용 Finance 키를 발급해 appstore.json 의 **reportsKey** 에 넣으면 배포 키를 건드리지 않아도 된다.',
1520
1523
  ].join(' '), {
1521
1524
  startDate: z.string().describe('시작일 YYYY-MM-DD (DAILY 가 아니면 이 값이 곧 reportDate)'),
1522
1525
  endDate: z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
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": {