@yoonion/mimi-seed-mcp 0.17.1 → 0.18.1
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/appstore/auth.d.ts +31 -0
- package/dist/appstore/auth.js +25 -1
- package/dist/appstore/sales.js +31 -11
- package/dist/auth/bigquery-auth.d.ts +1 -2
- package/dist/auth/bigquery-auth.js +2 -2
- package/dist/auth/bigquery-setup-cli.js +2 -2
- package/dist/auth/playstore-auth.d.ts +1 -1
- package/dist/auth/playstore-auth.js +2 -2
- package/dist/lib/google-auth-lite.d.ts +3 -0
- package/dist/lib/google-auth-lite.js +29 -0
- package/dist/lib/googleapis-lite.d.ts +17 -39
- package/dist/lib/googleapis-lite.js +81 -32
- package/dist/playstore/financials.js +2 -2
- package/dist/playstore/tools.d.ts +1 -2
- package/dist/playstore/tools.js +2 -2
- package/dist/registers/appstore.js +3 -0
- package/package.json +1 -1
package/dist/appstore/auth.d.ts
CHANGED
|
@@ -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>;
|
package/dist/appstore/auth.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
// jose 는 값 import 하지 않는다 — import 만으로 ~0.6초가 나가고, 이 파일은
|
|
2
|
+
// helpers.ts 를 통해 대부분의 register 에 딸려 들어가 MCP 기동 시간을 그대로 밀어올린다.
|
|
3
|
+
// 실제로 필요한 곳은 generateToken() 하나뿐이라 그 안에서 동적 import 한다.
|
|
2
4
|
import fs from 'node:fs';
|
|
3
5
|
import path from 'node:path';
|
|
4
6
|
import os from 'node:os';
|
|
@@ -41,6 +43,7 @@ function normalizePrivateKey(raw) {
|
|
|
41
43
|
return [header, ...chunks, footer, ''].join('\n');
|
|
42
44
|
}
|
|
43
45
|
export async function generateToken(creds) {
|
|
46
|
+
const { SignJWT, importPKCS8 } = await import('jose');
|
|
44
47
|
const normalizedKey = normalizePrivateKey(creds.privateKey);
|
|
45
48
|
let key;
|
|
46
49
|
try {
|
|
@@ -66,3 +69,24 @@ export async function getAuthHeaders() {
|
|
|
66
69
|
const token = await generateToken(creds);
|
|
67
70
|
return { Authorization: `Bearer ${token}` };
|
|
68
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* 매출 리포트용 헤더 — reportsKey 가 있으면 그걸로, 없으면 최상위 키로 폴백.
|
|
74
|
+
*
|
|
75
|
+
* 폴백이 조용하면 안 된다: 403 이 났을 때 "어느 키가 거부당했는지" 를 모르면 사용자가
|
|
76
|
+
* 엉뚱한 키의 롤을 들여다보게 된다. 그래서 어느 키를 썼는지 함께 돌려준다.
|
|
77
|
+
*/
|
|
78
|
+
export async function getReportsAuthHeaders() {
|
|
79
|
+
const creds = getAppStoreCredentials();
|
|
80
|
+
if (!creds)
|
|
81
|
+
return null;
|
|
82
|
+
if (creds.reportsKey) {
|
|
83
|
+
return {
|
|
84
|
+
headers: { Authorization: `Bearer ${await generateToken(creds.reportsKey)}` },
|
|
85
|
+
source: 'reportsKey',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
headers: { Authorization: `Bearer ${await generateToken(creds)}` },
|
|
90
|
+
source: 'default',
|
|
91
|
+
};
|
|
92
|
+
}
|
package/dist/appstore/sales.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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: { ...
|
|
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
|
-
'
|
|
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
|
-
|
|
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] };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { newJWT } from '../lib/google-auth-lite.js';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
@@ -55,7 +55,7 @@ export function deleteBigQueryServiceAccountJson() {
|
|
|
55
55
|
return true;
|
|
56
56
|
}
|
|
57
57
|
function makeJwt(sa) {
|
|
58
|
-
return
|
|
58
|
+
return newJWT({ email: sa.client_email, key: sa.private_key, scopes: BQ_SCOPES });
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* BigQuery 인증 클라이언트 해석. 우선순위:
|
|
@@ -3,7 +3,7 @@ import readline from 'node:readline';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { saveBigQueryServiceAccountJson, getBigQueryServiceAccountKey, } from './bigquery-auth.js';
|
|
5
5
|
import * as bigquery from '../bigquery/tools.js';
|
|
6
|
-
import {
|
|
6
|
+
import { newJWT } from '../lib/google-auth-lite.js';
|
|
7
7
|
import { resolveLang } from '../lib/lang.js';
|
|
8
8
|
// ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
|
|
9
9
|
const ko = {
|
|
@@ -111,7 +111,7 @@ async function main() {
|
|
|
111
111
|
if (projectId) {
|
|
112
112
|
console.log(M.probing(projectId));
|
|
113
113
|
try {
|
|
114
|
-
const jwt =
|
|
114
|
+
const jwt = newJWT({
|
|
115
115
|
email: parsed.client_email,
|
|
116
116
|
key: parsed.private_key,
|
|
117
117
|
scopes: [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { newJWT } from '../lib/google-auth-lite.js';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import os from 'node:os';
|
|
@@ -106,7 +106,7 @@ export function getServiceAccountClient(packageName) {
|
|
|
106
106
|
return null;
|
|
107
107
|
try {
|
|
108
108
|
const parsed = JSON.parse(json);
|
|
109
|
-
return
|
|
109
|
+
return newJWT({
|
|
110
110
|
email: parsed.client_email,
|
|
111
111
|
key: parsed.private_key,
|
|
112
112
|
// androidpublisher(edits/리스팅 등) + Developer Reporting(vitals 통계). 통계 도구가
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* google-auth-library 지연 로더 — googleapis-lite 와 같은 목적, 같은 기법.
|
|
3
|
+
*
|
|
4
|
+
* `import { JWT } from 'google-auth-library'` 는 그 자체로 ~0.6초다. 문제는 이게
|
|
5
|
+
* helpers.ts → 12개 register 로 전파돼, 도구를 하나도 호출하지 않아도 MCP 기동 때
|
|
6
|
+
* 무조건 지불된다는 점이다. 콜드 캐시에서는 이런 항목들이 합쳐져 기동이 25초까지 튀고,
|
|
7
|
+
* Claude Code 의 기본 5초 MCP 연결 타임아웃을 넘겨 서버가 통째로 등록되지 않는다.
|
|
8
|
+
*
|
|
9
|
+
* google-auth-library 는 `type: module` 도 `exports` 맵도 없는 순수 CJS 라
|
|
10
|
+
* `createRequire` 로 **동기** 로드가 된다. 덕분에 `getServiceAccountClient()` 처럼
|
|
11
|
+
* 이미 동기인 함수를 async 로 바꾸지 않고도 지연 로딩이 가능하다.
|
|
12
|
+
*
|
|
13
|
+
* 규칙:
|
|
14
|
+
* - `JWT` 를 **값으로** 쓰려면 이 파일의 `newJWT()` 를 쓴다.
|
|
15
|
+
* - 타입만 필요하면 `import type { JWT } from 'google-auth-library'` — 타입 import 는
|
|
16
|
+
* 런타임에 지워지므로 기동 비용이 0 이고 그대로 써도 된다.
|
|
17
|
+
* - 다른 파일에서 `import { JWT } from 'google-auth-library'` (값 import) 는 금지.
|
|
18
|
+
*/
|
|
19
|
+
import { createRequire } from 'node:module';
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
let cached;
|
|
22
|
+
function lib() {
|
|
23
|
+
cached ??= require('google-auth-library');
|
|
24
|
+
return cached;
|
|
25
|
+
}
|
|
26
|
+
/** `new JWT(opts)` 와 동일. 첫 호출에서만 google-auth-library 를 로드한다. */
|
|
27
|
+
export function newJWT(opts) {
|
|
28
|
+
return new (lib().JWT)(opts);
|
|
29
|
+
}
|
|
@@ -1,43 +1,21 @@
|
|
|
1
|
+
export type { youtube_v3 } from 'googleapis/build/src/apis/youtube/index.js';
|
|
1
2
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* `import { google } from 'googleapis'` 는 import 시점에 400여 개 API 클라이언트를
|
|
5
|
-
* 전부 로드해 그것만으로 ~19초를 쓴다. 그 결과 MCP 서버 기동(21~36초)이 Claude Code 의
|
|
6
|
-
* MCP 연결 타임아웃(30초)을 상습 초과해, 세션에서 mimi-seed 도구가 아예 등록되지 않는
|
|
7
|
-
* 사고가 났다 (2026-07-24). 실제 사용하는 API 만 서브패스로 로드하면 ~1초대다.
|
|
8
|
-
*
|
|
9
|
-
* - 새 Google API 가 필요하면 여기에 import 한 줄 + google 객체에 한 줄 추가한다.
|
|
10
|
-
* - 다른 파일에서 `from 'googleapis'` 값 import 는 금지 — 반드시 이 모듈을 거친다.
|
|
11
|
-
* (googleapis 는 exports map 이 없어 서브패스 import 가 공식적으로 가능하다.)
|
|
12
|
-
* - `auth` 는 AuthPlus 인스턴스라 `google.auth.OAuth2` 등 기존 사용처가 그대로 동작한다.
|
|
3
|
+
* 기존 `google.<api>(...)` 호출부와 100% 동일하게 동작하는 지연 로딩 네임스페이스.
|
|
4
|
+
* 각 프로퍼티는 getter 라서, 실제로 그 API 를 쓰는 도구가 호출될 때까지 아무것도 로드하지 않는다.
|
|
13
5
|
*/
|
|
14
|
-
import { admob } from 'googleapis/build/src/apis/admob/index.js';
|
|
15
|
-
import { analyticsadmin } from 'googleapis/build/src/apis/analyticsadmin/index.js';
|
|
16
|
-
import { analyticsdata } from 'googleapis/build/src/apis/analyticsdata/index.js';
|
|
17
|
-
import { androidpublisher } from 'googleapis/build/src/apis/androidpublisher/index.js';
|
|
18
|
-
import { bigquery } from 'googleapis/build/src/apis/bigquery/index.js';
|
|
19
|
-
import { billingbudgets } from 'googleapis/build/src/apis/billingbudgets/index.js';
|
|
20
|
-
import { cloudbilling } from 'googleapis/build/src/apis/cloudbilling/index.js';
|
|
21
|
-
import { cloudresourcemanager } from 'googleapis/build/src/apis/cloudresourcemanager/index.js';
|
|
22
|
-
import { firebase } from 'googleapis/build/src/apis/firebase/index.js';
|
|
23
|
-
import { iam } from 'googleapis/build/src/apis/iam/index.js';
|
|
24
|
-
import { searchconsole } from 'googleapis/build/src/apis/searchconsole/index.js';
|
|
25
|
-
import { serviceusage } from 'googleapis/build/src/apis/serviceusage/index.js';
|
|
26
|
-
import { youtube } from 'googleapis/build/src/apis/youtube/index.js';
|
|
27
|
-
export type { youtube_v3 } from 'googleapis/build/src/apis/youtube/index.js';
|
|
28
6
|
export declare const google: {
|
|
29
|
-
auth: import("googleapis
|
|
30
|
-
admob: typeof admob;
|
|
31
|
-
analyticsadmin: typeof analyticsadmin;
|
|
32
|
-
analyticsdata: typeof analyticsdata;
|
|
33
|
-
androidpublisher: typeof androidpublisher;
|
|
34
|
-
bigquery: typeof bigquery;
|
|
35
|
-
billingbudgets: typeof billingbudgets;
|
|
36
|
-
cloudbilling: typeof cloudbilling;
|
|
37
|
-
cloudresourcemanager: typeof cloudresourcemanager;
|
|
38
|
-
firebase: typeof firebase;
|
|
39
|
-
iam: typeof iam;
|
|
40
|
-
searchconsole: typeof searchconsole;
|
|
41
|
-
serviceusage: typeof serviceusage;
|
|
42
|
-
youtube: typeof youtube;
|
|
7
|
+
readonly auth: typeof import("googleapis/build/src/apis/firebase/index.js").auth;
|
|
8
|
+
readonly admob: typeof import("googleapis/build/src/apis/admob/index.js").admob;
|
|
9
|
+
readonly analyticsadmin: typeof import("googleapis/build/src/apis/analyticsadmin/index.js").analyticsadmin;
|
|
10
|
+
readonly analyticsdata: typeof import("googleapis/build/src/apis/analyticsdata/index.js").analyticsdata;
|
|
11
|
+
readonly androidpublisher: typeof import("googleapis/build/src/apis/androidpublisher/index.js").androidpublisher;
|
|
12
|
+
readonly bigquery: typeof import("googleapis/build/src/apis/bigquery/index.js").bigquery;
|
|
13
|
+
readonly billingbudgets: typeof import("googleapis/build/src/apis/billingbudgets/index.js").billingbudgets;
|
|
14
|
+
readonly cloudbilling: typeof import("googleapis/build/src/apis/cloudbilling/index.js").cloudbilling;
|
|
15
|
+
readonly cloudresourcemanager: typeof import("googleapis/build/src/apis/cloudresourcemanager/index.js").cloudresourcemanager;
|
|
16
|
+
readonly firebase: typeof import("googleapis/build/src/apis/firebase/index.js").firebase;
|
|
17
|
+
readonly iam: typeof import("googleapis/build/src/apis/iam/index.js").iam;
|
|
18
|
+
readonly searchconsole: typeof import("googleapis/build/src/apis/searchconsole/index.js").searchconsole;
|
|
19
|
+
readonly serviceusage: typeof import("googleapis/build/src/apis/serviceusage/index.js").serviceusage;
|
|
20
|
+
readonly youtube: typeof import("googleapis/build/src/apis/youtube/index.js").youtube;
|
|
43
21
|
};
|
|
@@ -3,40 +3,89 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `import { google } from 'googleapis'` 는 import 시점에 400여 개 API 클라이언트를
|
|
5
5
|
* 전부 로드해 그것만으로 ~19초를 쓴다. 그 결과 MCP 서버 기동(21~36초)이 Claude Code 의
|
|
6
|
-
* MCP 연결
|
|
7
|
-
* 사고가 났다 (2026-07-24). 실제 사용하는 API 만 서브패스로 로드하면 ~1초대다.
|
|
6
|
+
* MCP 연결 타임아웃을 상습 초과해, 세션에서 mimi-seed 도구가 아예 등록되지 않는
|
|
7
|
+
* 사고가 났다 (2026-07-24). 실제 사용하는 API 만 서브패스로 로드하면 ~1.4초대다.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* 그런데 그 ~1.4초도 **기동 시점에** 전부 나갔다. 서브패스를 정적 import 하면
|
|
10
|
+
* 도구를 하나도 호출하지 않아도 googleapis 공통 런타임(google-auth-library 등)이
|
|
11
|
+
* 딸려 온다. 콜드 캐시(Windows 실시간 검사 등)에서는 이게 25초까지 튀어
|
|
12
|
+
* 기본 5초 연결 타임아웃을 다시 넘겼다 (2026-08-22).
|
|
13
|
+
*
|
|
14
|
+
* 그래서 지금은 **첫 사용 시점까지 미룬다**:
|
|
15
|
+
* - googleapis 는 `type: module` 도 `exports` 맵도 없는 순수 CJS 라
|
|
16
|
+
* `createRequire` 로 **동기** 로드가 가능하다. 그래서 `google.admob(...)` 같은
|
|
17
|
+
* 기존 호출부를 async 로 바꿀 필요가 전혀 없다 — 소비자 코드는 그대로다.
|
|
18
|
+
* - 타입은 `typeof import(...)` 로 얻는다. 타입 위치의 import 는 런타임에 완전히
|
|
19
|
+
* 지워지므로 기동 비용이 0 이다. 절대 값 import 로 바꾸지 말 것 — 그 순간 이 파일의
|
|
20
|
+
* 존재 이유가 사라진다.
|
|
21
|
+
*
|
|
22
|
+
* 규칙:
|
|
23
|
+
* - 새 Google API 가 필요하면 아래 `google` 객체에 getter 한 줄을 추가한다.
|
|
10
24
|
* - 다른 파일에서 `from 'googleapis'` 값 import 는 금지 — 반드시 이 모듈을 거친다.
|
|
11
|
-
* (googleapis 는 exports map 이 없어 서브패스 import 가 공식적으로 가능하다.)
|
|
12
|
-
* - `auth` 는 AuthPlus 인스턴스라 `google.auth.OAuth2` 등 기존 사용처가 그대로 동작한다.
|
|
13
25
|
*/
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
import { createRequire } from 'node:module';
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
/** 서브패스 모듈 캐시 — require 자체도 캐시하지만 Map 조회가 더 싸다. */
|
|
29
|
+
const cache = new Map();
|
|
30
|
+
/**
|
|
31
|
+
* googleapis 서브패스를 첫 호출 시점에 동기 로드한다.
|
|
32
|
+
* 첫 호출이 공통 런타임까지 함께 지불하고(~1.1초), 이후 다른 서브패스는 ~20ms 다.
|
|
33
|
+
*/
|
|
34
|
+
function sub(name) {
|
|
35
|
+
let mod = cache.get(name);
|
|
36
|
+
if (mod === undefined) {
|
|
37
|
+
mod = require(`googleapis/build/src/apis/${name}/index.js`);
|
|
38
|
+
cache.set(name, mod);
|
|
39
|
+
}
|
|
40
|
+
return mod;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 기존 `google.<api>(...)` 호출부와 100% 동일하게 동작하는 지연 로딩 네임스페이스.
|
|
44
|
+
* 각 프로퍼티는 getter 라서, 실제로 그 API 를 쓰는 도구가 호출될 때까지 아무것도 로드하지 않는다.
|
|
45
|
+
*/
|
|
27
46
|
export const google = {
|
|
28
|
-
auth
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
// auth 는 AuthPlus 인스턴스. 어느 서브패스든 같은 걸 내보내지만,
|
|
48
|
+
// 기존 동작과 동일하게 firebase 서브패스에서 가져온다.
|
|
49
|
+
get auth() {
|
|
50
|
+
return sub('firebase').auth;
|
|
51
|
+
},
|
|
52
|
+
get admob() {
|
|
53
|
+
return sub('admob').admob;
|
|
54
|
+
},
|
|
55
|
+
get analyticsadmin() {
|
|
56
|
+
return sub('analyticsadmin').analyticsadmin;
|
|
57
|
+
},
|
|
58
|
+
get analyticsdata() {
|
|
59
|
+
return sub('analyticsdata').analyticsdata;
|
|
60
|
+
},
|
|
61
|
+
get androidpublisher() {
|
|
62
|
+
return sub('androidpublisher').androidpublisher;
|
|
63
|
+
},
|
|
64
|
+
get bigquery() {
|
|
65
|
+
return sub('bigquery').bigquery;
|
|
66
|
+
},
|
|
67
|
+
get billingbudgets() {
|
|
68
|
+
return sub('billingbudgets').billingbudgets;
|
|
69
|
+
},
|
|
70
|
+
get cloudbilling() {
|
|
71
|
+
return sub('cloudbilling').cloudbilling;
|
|
72
|
+
},
|
|
73
|
+
get cloudresourcemanager() {
|
|
74
|
+
return sub('cloudresourcemanager').cloudresourcemanager;
|
|
75
|
+
},
|
|
76
|
+
get firebase() {
|
|
77
|
+
return sub('firebase').firebase;
|
|
78
|
+
},
|
|
79
|
+
get iam() {
|
|
80
|
+
return sub('iam').iam;
|
|
81
|
+
},
|
|
82
|
+
get searchconsole() {
|
|
83
|
+
return sub('searchconsole').searchconsole;
|
|
84
|
+
},
|
|
85
|
+
get serviceusage() {
|
|
86
|
+
return sub('serviceusage').serviceusage;
|
|
87
|
+
},
|
|
88
|
+
get youtube() {
|
|
89
|
+
return sub('youtube').youtube;
|
|
90
|
+
},
|
|
42
91
|
};
|
|
@@ -11,7 +11,7 @@ import fs from 'node:fs';
|
|
|
11
11
|
import os from 'node:os';
|
|
12
12
|
import path from 'node:path';
|
|
13
13
|
import zlib from 'node:zlib';
|
|
14
|
-
import {
|
|
14
|
+
import { newJWT } from '../lib/google-auth-lite.js';
|
|
15
15
|
import { fetchWithTimeout, HTTP_TRANSFER_TIMEOUT_MS } from '../lib/http.js';
|
|
16
16
|
import { requireServiceAccountJson } from '../helpers.js';
|
|
17
17
|
const CONFIG_PATH = path.join(os.homedir(), '.mimi-seed', 'play-financials.json');
|
|
@@ -68,7 +68,7 @@ function normalizeBucket(raw) {
|
|
|
68
68
|
*/
|
|
69
69
|
function storageClient(packageName) {
|
|
70
70
|
const parsed = JSON.parse(requireServiceAccountJson(packageName));
|
|
71
|
-
return
|
|
71
|
+
return newJWT({
|
|
72
72
|
email: parsed.client_email,
|
|
73
73
|
key: parsed.private_key,
|
|
74
74
|
scopes: [STORAGE_SCOPE],
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { OAuth2Client } from 'google-auth-library';
|
|
2
|
-
import { JWT } from 'google-auth-library';
|
|
1
|
+
import type { OAuth2Client, JWT } from 'google-auth-library';
|
|
3
2
|
export type PlayImageType = 'featureGraphic' | 'icon' | 'phoneScreenshots' | 'promoGraphic' | 'sevenInchScreenshots' | 'tenInchScreenshots' | 'tvBanner' | 'tvScreenshots' | 'wearScreenshots';
|
|
4
3
|
/**
|
|
5
4
|
* Google Play Developer API (Android Publisher API v3) 래퍼
|
package/dist/playstore/tools.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { google } from '../lib/googleapis-lite.js';
|
|
2
|
-
import {
|
|
2
|
+
import { newJWT } from '../lib/google-auth-lite.js';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { extractHttpStatus } from '../lib/google-errors.js';
|
|
5
5
|
function mimeTypeFor(filePath) {
|
|
@@ -763,7 +763,7 @@ export async function verifyServiceAccountJson(serviceAccountJson, packageName)
|
|
|
763
763
|
message: `Expected type="service_account", got "${parsed.type}" — make sure you downloaded a service account key, not an OAuth client.`,
|
|
764
764
|
};
|
|
765
765
|
}
|
|
766
|
-
const jwt =
|
|
766
|
+
const jwt = newJWT({
|
|
767
767
|
email: parsed.client_email,
|
|
768
768
|
key: parsed.private_key,
|
|
769
769
|
scopes: ['https://www.googleapis.com/auth/androidpublisher'],
|
|
@@ -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