@yoonion/mimi-seed-mcp 0.19.18 → 0.20.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.
- package/README.md +17 -10
- package/assets/agent-guide.md +108 -16
- package/dist/admob/tools.d.ts +16 -15
- package/dist/admob/tools.js +6 -5
- package/dist/android/keystore-store.d.ts +30 -0
- package/dist/android/keystore-store.js +85 -0
- package/dist/android/keystore.d.ts +3 -0
- package/dist/android/keystore.js +12 -3
- package/dist/android/messages.d.ts +35 -0
- package/dist/android/messages.js +145 -0
- package/dist/android/playstore-sa.d.ts +18 -0
- package/dist/android/playstore-sa.js +15 -0
- package/dist/appstore/analytics.js +5 -4
- package/dist/appstore/apps.d.ts +81 -0
- package/dist/appstore/apps.js +174 -0
- package/dist/appstore/client.d.ts +21 -0
- package/dist/appstore/client.js +130 -0
- package/dist/appstore/declarations.js +9 -8
- package/dist/appstore/messages.d.ts +15 -0
- package/dist/appstore/messages.js +104 -0
- package/dist/appstore/previews.js +5 -4
- package/dist/appstore/product-localization.js +5 -4
- package/dist/appstore/product-review.js +8 -7
- package/dist/appstore/products.d.ts +66 -0
- package/dist/appstore/products.js +305 -0
- package/dist/appstore/release.js +6 -5
- package/dist/appstore/review-submission.d.ts +109 -0
- package/dist/appstore/review-submission.js +468 -0
- package/dist/appstore/screenshots.d.ts +11 -1
- package/dist/appstore/screenshots.js +5 -5
- package/dist/appstore/testflight.js +14 -13
- package/dist/appstore/tools.d.ts +16 -323
- package/dist/appstore/tools.js +16 -1318
- package/dist/appstore/types.d.ts +35 -0
- package/dist/appstore/types.js +7 -0
- package/dist/appstore/versions.d.ts +113 -0
- package/dist/appstore/versions.js +263 -0
- package/dist/auth/google-auth.d.ts +7 -0
- package/dist/auth/google-auth.js +61 -8
- package/dist/auth/playstore-auth.d.ts +6 -0
- package/dist/auth/playstore-auth.js +22 -3
- package/dist/bigquery/ids.d.ts +4 -0
- package/dist/bigquery/ids.js +30 -0
- package/dist/bigquery/tools.d.ts +20 -2
- package/dist/bigquery/tools.js +134 -6
- package/dist/billing/tools.js +4 -4
- package/dist/checks/plan.js +1 -1
- package/dist/checks/release-doctor.d.ts +5 -1
- package/dist/checks/release-doctor.js +728 -17
- package/dist/ci/github.d.ts +4 -4
- package/dist/ci/github.js +7 -6
- package/dist/ci/gitlab.js +3 -2
- package/dist/facebook/api.js +7 -6
- package/dist/firebase/remote-config.js +5 -3
- package/dist/firebase/tools.d.ts +1 -1
- package/dist/firebase/tools.js +34 -25
- package/dist/ga4/tools.js +4 -6
- package/dist/googleads/tools.d.ts +14 -14
- package/dist/googleads/tools.js +2 -2
- package/dist/gsc/tools.js +9 -4
- package/dist/iam/key-files.d.ts +14 -0
- package/dist/iam/key-files.js +49 -0
- package/dist/iam/tools.d.ts +10 -0
- package/dist/iam/tools.js +31 -10
- package/dist/instagram/api.js +9 -8
- package/dist/jenkins/config.d.ts +5 -0
- package/dist/jenkins/config.js +40 -0
- package/dist/jenkins/credentials.d.ts +40 -2
- package/dist/jenkins/credentials.js +130 -24
- package/dist/jenkins/messages.d.ts +26 -0
- package/dist/jenkins/messages.js +105 -0
- package/dist/lib/atomic-write.d.ts +27 -3
- package/dist/lib/atomic-write.js +41 -2
- package/dist/lib/google-timeouts.d.ts +47 -0
- package/dist/lib/google-timeouts.js +55 -0
- package/dist/lib/googleapis-lite.d.ts +11 -0
- package/dist/lib/googleapis-lite.js +6 -0
- package/dist/lib/mcp-response.d.ts +5 -0
- package/dist/lib/mcp-response.js +9 -2
- package/dist/lib/package-name.d.ts +15 -0
- package/dist/lib/package-name.js +42 -0
- package/dist/lib/package-root.d.ts +33 -0
- package/dist/lib/paginate.d.ts +19 -0
- package/dist/lib/paginate.js +38 -0
- package/dist/lib/path-containment.d.ts +4 -0
- package/dist/lib/path-containment.js +39 -0
- package/dist/lib/resource-id.d.ts +18 -0
- package/dist/lib/resource-id.js +85 -0
- package/dist/lib/tool-registrar.d.ts +64 -0
- package/dist/lib/tool-registrar.js +163 -0
- package/dist/lib/toolsets.d.ts +39 -0
- package/dist/lib/toolsets.js +104 -0
- package/dist/lib/url-path.d.ts +1 -0
- package/dist/lib/url-path.js +16 -0
- package/dist/lib/wrap-domain.d.ts +2 -0
- package/dist/lib/wrap-domain.js +35 -0
- package/dist/playstore/data-safety.d.ts +16 -0
- package/dist/playstore/data-safety.js +38 -0
- package/dist/playstore/edits.d.ts +13 -0
- package/dist/playstore/edits.js +53 -0
- package/dist/playstore/images.d.ts +21 -0
- package/dist/playstore/images.js +73 -0
- package/dist/playstore/listing.d.ts +14 -0
- package/dist/playstore/listing.js +84 -0
- package/dist/playstore/messages.d.ts +48 -0
- package/dist/playstore/messages.js +173 -0
- package/dist/playstore/products.d.ts +52 -0
- package/dist/playstore/products.js +152 -0
- package/dist/playstore/recovery.d.ts +39 -0
- package/dist/playstore/recovery.js +76 -0
- package/dist/playstore/releases.d.ts +56 -0
- package/dist/playstore/releases.js +250 -0
- package/dist/playstore/reviews.d.ts +12 -0
- package/dist/playstore/reviews.js +27 -0
- package/dist/playstore/service-account.d.ts +11 -0
- package/dist/playstore/service-account.js +67 -0
- package/dist/playstore/statistics.d.ts +16 -0
- package/dist/playstore/statistics.js +66 -0
- package/dist/playstore/tools.d.ts +24 -223
- package/dist/playstore/tools.js +24 -820
- package/dist/prompts.js +1 -1
- package/dist/registers/admob.d.ts +2 -2
- package/dist/registers/admob.js +16 -26
- package/dist/registers/ai.d.ts +2 -2
- package/dist/registers/ai.js +3 -2
- package/dist/registers/android.d.ts +2 -2
- package/dist/registers/android.js +59 -158
- package/dist/registers/appstore.d.ts +2 -2
- package/dist/registers/appstore.js +218 -477
- package/dist/registers/auth.d.ts +2 -2
- package/dist/registers/auth.js +37 -55
- package/dist/registers/bigquery.d.ts +2 -2
- package/dist/registers/bigquery.js +27 -35
- package/dist/registers/billing.d.ts +2 -2
- package/dist/registers/billing.js +11 -18
- package/dist/registers/checks.d.ts +2 -2
- package/dist/registers/checks.js +8 -7
- package/dist/registers/ci.d.ts +2 -2
- package/dist/registers/ci.js +26 -41
- package/dist/registers/facebook.d.ts +2 -2
- package/dist/registers/facebook.js +26 -53
- package/dist/registers/firebase.d.ts +2 -2
- package/dist/registers/firebase.js +16 -41
- package/dist/registers/ga4.d.ts +2 -2
- package/dist/registers/ga4.js +5 -23
- package/dist/registers/googleads.d.ts +2 -2
- package/dist/registers/googleads.js +40 -60
- package/dist/registers/gsc.d.ts +2 -2
- package/dist/registers/gsc.js +6 -11
- package/dist/registers/iam.d.ts +2 -2
- package/dist/registers/iam.js +36 -56
- package/dist/registers/instagram.d.ts +2 -2
- package/dist/registers/instagram.js +18 -33
- package/dist/registers/jenkins.d.ts +2 -2
- package/dist/registers/jenkins.js +51 -53
- package/dist/registers/playstore.d.ts +2 -2
- package/dist/registers/playstore.js +130 -353
- package/dist/registers/threads.d.ts +2 -2
- package/dist/registers/threads.js +29 -54
- package/dist/registers/tiktok.d.ts +2 -2
- package/dist/registers/tiktok.js +0 -1
- package/dist/registers/video.d.ts +2 -2
- package/dist/registers/video.js +21 -23
- package/dist/registers/youtube.d.ts +2 -2
- package/dist/remote-sync.d.ts +17 -0
- package/dist/remote-sync.js +53 -3
- package/dist/resources.d.ts +44 -1
- package/dist/resources.js +68 -17
- package/dist/server.d.ts +3 -1
- package/dist/server.js +35 -24
- package/dist/threads/api.js +4 -3
- package/dist/video/render.d.ts +15 -0
- package/dist/video/render.js +58 -6
- package/dist/video/schemas.d.ts +10 -10
- package/dist/video/youtube-publish.js +5 -3
- package/dist/youtube/tools.js +2 -1
- package/package.json +15 -14
- package/tool-manifest.json +442 -7
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Android 서명/Jenkins 셋업 도구의 사람용 응답 문구 — register 는 판단만 하고, 긴 단계별
|
|
2
|
+
// 안내는 여기서 조립한다. 반환값은 줄 배열이다 (textResult 가 '\n' 으로 합친다).
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { isGenericCredentialPrefix } from '../jenkins/messages.js';
|
|
5
|
+
import { KEYSTORE_FILE, SIGNING_SECRETS_FILE } from './keystore-store.js';
|
|
6
|
+
/** 기본 id 접두사가 흔한 이름이면 계획 맨 앞에 붙이는 경고 (없으면 빈 배열). */
|
|
7
|
+
function genericPrefixWarning(packageName, prefix) {
|
|
8
|
+
if (!isGenericCredentialPrefix(prefix))
|
|
9
|
+
return [];
|
|
10
|
+
return [
|
|
11
|
+
`⚠️ 아래 기본 credential id 접두사 "${prefix}" 는 ${packageName} 의 마지막 세그먼트만으로 만든 흔한 이름이라 ` +
|
|
12
|
+
'같은 Jenkins 의 다른 앱 credential 과 겹칠 수 있다. 기존 id 라는 dry-run 이 나오면 설명을 확인하고, 필요하면 고유한 id 를 쓰세요.',
|
|
13
|
+
'',
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
/** keystore·비밀번호 **파일 경로**로 Jenkins 에 등록하는 네 단계 (start 번부터). */
|
|
17
|
+
function pathRegistrationSteps(prefix, keystorePath, secretsPath, start) {
|
|
18
|
+
return [
|
|
19
|
+
` ${start}. jenkins_upload_keystore(id="${prefix}-android-keystore", keystore_path="${keystorePath}", file_name="upload.jks")`,
|
|
20
|
+
` ${start + 1}. jenkins_create_credential(id="${prefix}-android-store-password", secret_file="${secretsPath}", secret_field="storePassword")`,
|
|
21
|
+
` ${start + 2}. jenkins_create_credential(id="${prefix}-android-key-alias", secret_file="${secretsPath}", secret_field="keyAlias")`,
|
|
22
|
+
` ${start + 3}. jenkins_create_credential(id="${prefix}-android-key-password", secret_file="${secretsPath}", secret_field="keyPassword")`,
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* android_signing_setup — Play Console 에 이미 있는 앱. 기존 keystore 를 받아 등록하는 순서.
|
|
27
|
+
*
|
|
28
|
+
* 비밀값(keystore 바이트·비밀번호)을 대화에 싣지 않는다 — 사용자가 keystore 를 `keystoreDir` 로 복사하고
|
|
29
|
+
* 비밀번호를 그 폴더의 signing.json 에 적게 한 뒤, Jenkins 도구에는 경로만 넘긴다
|
|
30
|
+
* (jenkins_upload_keystore.keystore_path, jenkins_create_credential.secret_file + secret_field).
|
|
31
|
+
*/
|
|
32
|
+
export function existingAppSigningPlanLines(packageName, prefix, jenkinsStatus, keystoreDir) {
|
|
33
|
+
const keystorePath = path.join(keystoreDir, KEYSTORE_FILE);
|
|
34
|
+
const secretsPath = path.join(keystoreDir, SIGNING_SECRETS_FILE);
|
|
35
|
+
return [
|
|
36
|
+
`📦 ${packageName} — **기존 앱** (Play Console에 이미 존재)`,
|
|
37
|
+
'',
|
|
38
|
+
'기존 upload keystore와 비밀번호가 있어야 합니다.',
|
|
39
|
+
'분실한 경우 새 앱으로 다시 등록하거나 Play App Signing으로 마이그레이션해야 합니다.',
|
|
40
|
+
'',
|
|
41
|
+
`Jenkins: ${jenkinsStatus}`,
|
|
42
|
+
'',
|
|
43
|
+
...genericPrefixWarning(packageName, prefix),
|
|
44
|
+
'── 준비 (사용자가 직접 — 비밀번호·keystore 를 대화에 붙여넣지 않는다) ──',
|
|
45
|
+
` 1. 폴더를 만든다: ${keystoreDir}`,
|
|
46
|
+
` 2. 기존 upload keystore 를 복사한다 → ${keystorePath} (.jks / .keystore / .p12)`,
|
|
47
|
+
` 3. 같은 폴더에 비밀번호 파일을 만든다 → ${secretsPath}`,
|
|
48
|
+
' 내용: {"storePassword": "…", "keyAlias": "…", "keyPassword": "…"}',
|
|
49
|
+
' 4. 두 파일을 본인만 읽게 한다 (macOS/Linux: chmod 600)',
|
|
50
|
+
' 5. Play Store SA JSON 이 없으면 setup_playstore_connection 으로 만든다',
|
|
51
|
+
'',
|
|
52
|
+
'── 등록 순서 (값이 아니라 경로를 넘긴다) ────────────',
|
|
53
|
+
...pathRegistrationSteps(prefix, keystorePath, secretsPath, 1),
|
|
54
|
+
` 5. jenkins_upload_playstore_sa(package_name="${packageName}", credential_id="${prefix}-playstore-sa")`,
|
|
55
|
+
` └ SA JSON이 없으면 먼저: setup_playstore_connection(packageName="${packageName}", projectId="...")`,
|
|
56
|
+
'',
|
|
57
|
+
'사용자가 비밀번호를 채팅에 적었다면 secret 파라미터로 옮기지 말고, 위 signing.json 에 저장하도록 안내하세요.',
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
/** android_signing_setup — 신규 앱(또는 Play Console 확인 불가). keystore 생성부터의 순서. */
|
|
61
|
+
export function newAppSigningPlanLines(opts) {
|
|
62
|
+
const { packageName, prefix, appStatus, playNote, jenkinsStatus, jenkinsConfigured, keytoolOk, projectId, keystoreDir } = opts;
|
|
63
|
+
const keystorePath = path.join(keystoreDir, KEYSTORE_FILE);
|
|
64
|
+
const secretsPath = path.join(keystoreDir, SIGNING_SECRETS_FILE);
|
|
65
|
+
const appLabel = appStatus === 'new' ? '**신규 앱**' : '**신규 앱으로 처리** (Play Console 확인 불가)';
|
|
66
|
+
return [
|
|
67
|
+
`📦 ${packageName} — ${appLabel}`,
|
|
68
|
+
playNote ? ` ${playNote}` : '',
|
|
69
|
+
'',
|
|
70
|
+
...genericPrefixWarning(packageName, prefix),
|
|
71
|
+
`Jenkins: ${jenkinsStatus}`,
|
|
72
|
+
`keytool(Java JDK): ${keytoolOk ? '✅ 설치됨 — 자동 생성 가능' : '❌ 미설치 — android_generate_keystore 호출 불가, 수동 생성 필요'}`,
|
|
73
|
+
'',
|
|
74
|
+
'── 신규 앱 설정 순서 ─────────────────────────────',
|
|
75
|
+
jenkinsConfigured ? '' : ' 0. jenkins_status → jenkins_save_config (Jenkins 먼저 설정)',
|
|
76
|
+
keytoolOk
|
|
77
|
+
? ` 1. android_generate_keystore(app_name="${prefix}") → keystore + 비밀번호를 ~/.mimi-seed/keystores/ 에 파일로 생성`
|
|
78
|
+
: ` 1. ⚠️ 사용자가 직접 keytool -genkeypair 로 만든 keystore 를 ${keystorePath} 에, 비밀번호를 ${secretsPath} ({"storePassword","keyAlias","keyPassword"}) 에 저장 — 대화에 붙여넣지 않는다`,
|
|
79
|
+
...(keytoolOk
|
|
80
|
+
? [` 2~5. android_generate_keystore 응답의 경로로 jenkins_upload_keystore(keystore_path=…) + jenkins_create_credential(secret_file=…, secret_field=storePassword|keyAlias|keyPassword) — id 는 "${prefix}-android-keystore" / "-store-password" / "-key-alias" / "-key-password"`]
|
|
81
|
+
: pathRegistrationSteps(prefix, keystorePath, secretsPath, 2)),
|
|
82
|
+
projectId
|
|
83
|
+
? ` 6. setup_playstore_connection(packageName="${packageName}", projectId="${projectId}")`
|
|
84
|
+
: ` 6. setup_playstore_connection(packageName="${packageName}", projectId="<GCP 프로젝트 ID>")`,
|
|
85
|
+
' └ GCP 프로젝트 ID를 모르면 사용자에게 확인하세요.',
|
|
86
|
+
` 7. jenkins_upload_playstore_sa(package_name="${packageName}", credential_id="${prefix}-playstore-sa")`,
|
|
87
|
+
' 8. Play Console에서 서비스 계정 초대 (수동, 1회)',
|
|
88
|
+
' → Play Console → 사용자 및 권한 → SA 이메일 → 릴리즈 관리자 권한 부여',
|
|
89
|
+
' 9. 첫 AAB 빌드 후 Play Console에 내부 테스트용으로 수동 업로드 (신규 앱 첫 번째만)',
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
/** android_generate_keystore — keytool 이 없을 때 설치 안내. */
|
|
93
|
+
export function keytoolMissingLines() {
|
|
94
|
+
return [
|
|
95
|
+
'❌ keytool이 설치되지 않았습니다.',
|
|
96
|
+
'',
|
|
97
|
+
'Java JDK를 설치하면 keytool이 포함됩니다:',
|
|
98
|
+
' macOS: brew install openjdk',
|
|
99
|
+
' Ubuntu: sudo apt install default-jdk',
|
|
100
|
+
' Windows: https://adoptium.net/',
|
|
101
|
+
'',
|
|
102
|
+
'설치 후 다시 android_generate_keystore를 호출하세요.',
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
/** android_generate_keystore — 생성 완료. 비밀값은 싣지 않고 경로만 넘긴다. */
|
|
106
|
+
export function keystoreGeneratedLines(prefix, saved) {
|
|
107
|
+
return [
|
|
108
|
+
'✅ Android upload keystore 생성 완료 — 비밀번호와 keystore 는 파일로만 저장했습니다 (응답에 싣지 않음).',
|
|
109
|
+
'',
|
|
110
|
+
`keystore: ${saved.keystorePath} (0600)`,
|
|
111
|
+
`비밀번호 파일: ${saved.secretsPath} (0600 — keyAlias / storePassword / keyPassword)`,
|
|
112
|
+
`keyAlias: ${saved.keyAlias}`,
|
|
113
|
+
'',
|
|
114
|
+
'🔒 분실하면 앱 서명을 영구히 잃습니다. 위 폴더를 비밀번호 관리자 등 안전한 곳에 백업하세요.',
|
|
115
|
+
'',
|
|
116
|
+
'── 다음 단계 — 값을 복사하지 말고 경로를 넘기세요 ──',
|
|
117
|
+
` jenkins_upload_keystore(id="${prefix}-android-keystore", keystore_path="${saved.keystorePath}", file_name="upload.jks")`,
|
|
118
|
+
` jenkins_create_credential(id="${prefix}-android-store-password", secret_file="${saved.secretsPath}", secret_field="storePassword")`,
|
|
119
|
+
` jenkins_create_credential(id="${prefix}-android-key-alias", secret_file="${saved.secretsPath}", secret_field="keyAlias")`,
|
|
120
|
+
` jenkins_create_credential(id="${prefix}-android-key-password", secret_file="${saved.secretsPath}", secret_field="keyPassword")`,
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
/** jenkins_upload_playstore_sa — 패키지별 SA 파일이 아직 없을 때. */
|
|
124
|
+
export function playServiceAccountMissingLines(packageName, saPath) {
|
|
125
|
+
return [
|
|
126
|
+
`❌ ${packageName} 서비스 계정 JSON이 없습니다.`,
|
|
127
|
+
` 경로: ${saPath}`,
|
|
128
|
+
'',
|
|
129
|
+
'먼저 setup_playstore_connection을 호출해 서비스 계정을 생성하세요.',
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
/** jenkins_upload_playstore_sa — 업로드 완료 + Play Console 권한 부여(수동 1회) 안내. */
|
|
133
|
+
export function playServiceAccountUploadedLines(opts) {
|
|
134
|
+
const { result, credentialId, clientEmail, rawLength } = opts;
|
|
135
|
+
return [
|
|
136
|
+
`✅ Play Store SA JSON → Jenkins ${result}: \`${credentialId}\``,
|
|
137
|
+
` 서비스 계정: ${clientEmail}`,
|
|
138
|
+
` 파일 크기: ${rawLength}자`,
|
|
139
|
+
'',
|
|
140
|
+
'다음 단계:',
|
|
141
|
+
' Play Console → 설정 → 사용자 및 권한 → 서비스 계정에서',
|
|
142
|
+
` ${clientEmail} 를 찾아 "릴리즈 관리자" 권한을 부여하세요.`,
|
|
143
|
+
' (수동 1회 작업)',
|
|
144
|
+
];
|
|
145
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jenkins_upload_playstore_sa 가 올릴 Play 서비스 계정 JSON 을 고른다.
|
|
3
|
+
*
|
|
4
|
+
* - explicitPath 가 있으면 iam_create_key 가 저장한 키 파일(~/.mimi-seed/keys/ 안만 허용)을 읽는다.
|
|
5
|
+
* - 없으면 패키지별 등록 SA(~/.mimi-seed/play-service-accounts/<pkg>.json)를 읽는다. 그 파일이
|
|
6
|
+
* 없으면 `found: false` 로 경로만 돌려준다 — register 가 "먼저 setup_playstore_connection" 을 안내한다.
|
|
7
|
+
*
|
|
8
|
+
* clientEmail 은 안내문용이다. JSON 이 깨져 있어도 업로드 자체는 막지 않는다 (예전 동작 그대로).
|
|
9
|
+
*/
|
|
10
|
+
export type PlayServiceAccountSource = {
|
|
11
|
+
found: false;
|
|
12
|
+
path: string;
|
|
13
|
+
} | {
|
|
14
|
+
found: true;
|
|
15
|
+
raw: string;
|
|
16
|
+
clientEmail: string;
|
|
17
|
+
};
|
|
18
|
+
export declare function loadPlayServiceAccountForUpload(packageName: string, explicitPath?: string): PlayServiceAccountSource;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { serviceAccountPathForPackage } from '../auth/playstore-auth.js';
|
|
3
|
+
import { readServiceAccountKeyFile } from '../iam/key-files.js';
|
|
4
|
+
export function loadPlayServiceAccountForUpload(packageName, explicitPath) {
|
|
5
|
+
const saPath = explicitPath ?? serviceAccountPathForPackage(packageName);
|
|
6
|
+
if (!explicitPath && !existsSync(saPath))
|
|
7
|
+
return { found: false, path: saPath };
|
|
8
|
+
const raw = explicitPath ? readServiceAccountKeyFile(explicitPath) : readFileSync(saPath, 'utf-8');
|
|
9
|
+
let clientEmail = '(파싱 실패)';
|
|
10
|
+
try {
|
|
11
|
+
clientEmail = JSON.parse(raw).client_email ?? clientEmail;
|
|
12
|
+
}
|
|
13
|
+
catch { /* ignore */ }
|
|
14
|
+
return { found: true, raw, clientEmail };
|
|
15
|
+
}
|
|
@@ -4,6 +4,7 @@ import { getReportsAuthHeaders } from './auth.js';
|
|
|
4
4
|
import { V1_BASE, apiRequest, authHeadersOrThrow } from './http.js';
|
|
5
5
|
import { fetchWithTimeout, HTTP_TRANSFER_TIMEOUT_MS } from '../lib/http.js';
|
|
6
6
|
import { parseTsv } from './sales.js';
|
|
7
|
+
import { encodePathSegment } from '../lib/url-path.js';
|
|
7
8
|
export function selectWeeklyAnalyticsReports(reports) {
|
|
8
9
|
const standard = reports.filter((row) => !/Detailed/i.test(row.attributes?.name ?? ''));
|
|
9
10
|
const detailed = reports.filter((row) => /Detailed/i.test(row.attributes?.name ?? ''));
|
|
@@ -97,14 +98,14 @@ export function selectLatestInstanceRows(batches) {
|
|
|
97
98
|
.flatMap(([, value]) => value.rows);
|
|
98
99
|
}
|
|
99
100
|
async function rowsForReport(reportId) {
|
|
100
|
-
const instances = await reportGet(`/analyticsReports/${reportId}/instances`, { 'filter[granularity]': 'WEEKLY', limit: '200' });
|
|
101
|
+
const instances = await reportGet(`/analyticsReports/${encodePathSegment(reportId)}/instances`, { 'filter[granularity]': 'WEEKLY', limit: '200' });
|
|
101
102
|
const latest = (instances.data ?? [])
|
|
102
103
|
.filter((row) => row.attributes?.processingDate)
|
|
103
104
|
.sort((a, b) => (b.attributes?.processingDate ?? '').localeCompare(a.attributes?.processingDate ?? ''))
|
|
104
105
|
.slice(0, MAX_WEEKLY_INSTANCES);
|
|
105
106
|
const batches = [];
|
|
106
107
|
for (const instance of latest) {
|
|
107
|
-
const segments = await reportGet(`/analyticsReportInstances/${instance.id}/segments`, { limit: '200' });
|
|
108
|
+
const segments = await reportGet(`/analyticsReportInstances/${encodePathSegment(instance.id)}/segments`, { limit: '200' });
|
|
108
109
|
const rows = [];
|
|
109
110
|
for (const segment of segments.data ?? [])
|
|
110
111
|
rows.push(...await downloadSegment(segment));
|
|
@@ -260,7 +261,7 @@ export function buildWeeklyInsight(input) {
|
|
|
260
261
|
};
|
|
261
262
|
}
|
|
262
263
|
export async function getWeeklyInsight(input) {
|
|
263
|
-
const requests = await reportGet(`/apps/${input.appId}/analyticsReportRequests`, { 'filter[accessType]': 'ONGOING', limit: '10' });
|
|
264
|
+
const requests = await reportGet(`/apps/${encodePathSegment(input.appId)}/analyticsReportRequests`, { 'filter[accessType]': 'ONGOING', limit: '10' });
|
|
264
265
|
const active = (requests.data ?? []).find((row) => !row.attributes?.stoppedDueToInactivity);
|
|
265
266
|
if (!active) {
|
|
266
267
|
if (!input.createIfMissing) {
|
|
@@ -283,7 +284,7 @@ export async function getWeeklyInsight(input) {
|
|
|
283
284
|
message: 'ONGOING Analytics report request를 만들었습니다. 최초 데이터는 보통 24~48시간 뒤 생성됩니다.',
|
|
284
285
|
};
|
|
285
286
|
}
|
|
286
|
-
const reports = await reportGet(`/analyticsReportRequests/${active.id}/reports`, { limit: '200' });
|
|
287
|
+
const reports = await reportGet(`/analyticsReportRequests/${encodePathSegment(active.id)}/reports`, { limit: '200' });
|
|
287
288
|
const { engagement, downloads, purchases } = selectWeeklyAnalyticsReports(reports.data ?? []);
|
|
288
289
|
if (!engagement && !downloads && !purchases) {
|
|
289
290
|
return {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { AscResource, AscToOne } from './types.js';
|
|
2
|
+
interface AppAttributes {
|
|
3
|
+
name?: string;
|
|
4
|
+
bundleId?: string;
|
|
5
|
+
sku?: string;
|
|
6
|
+
primaryLocale?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function listApps(): Promise<{
|
|
9
|
+
id: string;
|
|
10
|
+
name: string | undefined;
|
|
11
|
+
bundleId: string | undefined;
|
|
12
|
+
sku: string | undefined;
|
|
13
|
+
primaryLocale: string | undefined;
|
|
14
|
+
}[]>;
|
|
15
|
+
export declare function getApp(appId: string): Promise<AscResource<AppAttributes, {
|
|
16
|
+
[x: string]: AscToOne | import("./types.js").AscToMany | undefined;
|
|
17
|
+
}> | undefined>;
|
|
18
|
+
export declare function listBetaGroups(appId: string): Promise<{
|
|
19
|
+
id: string;
|
|
20
|
+
name: string | undefined;
|
|
21
|
+
isInternal: boolean | undefined;
|
|
22
|
+
publicLink: string | undefined;
|
|
23
|
+
publicLinkEnabled: boolean | undefined;
|
|
24
|
+
}[]>;
|
|
25
|
+
export declare function getAppInfo(appId: string): Promise<{
|
|
26
|
+
id: string;
|
|
27
|
+
state: string | undefined;
|
|
28
|
+
ageRating: string | undefined;
|
|
29
|
+
}[]>;
|
|
30
|
+
export interface AppInfoLocalizationFields {
|
|
31
|
+
name?: string;
|
|
32
|
+
subtitle?: string;
|
|
33
|
+
privacyPolicyUrl?: string;
|
|
34
|
+
privacyPolicyText?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare function listAppInfoLocalizations(appId: string, locale?: string): Promise<{
|
|
37
|
+
appInfoId: string;
|
|
38
|
+
appInfoState: string;
|
|
39
|
+
localizations: {
|
|
40
|
+
id: string;
|
|
41
|
+
locale: string | undefined;
|
|
42
|
+
name: string | undefined;
|
|
43
|
+
subtitle: string | undefined;
|
|
44
|
+
privacyPolicyUrl: string | undefined;
|
|
45
|
+
privacyPolicyText: string | undefined;
|
|
46
|
+
}[];
|
|
47
|
+
}>;
|
|
48
|
+
export declare function updateAppInfoLocalization(localizationId: string, fields: AppInfoLocalizationFields): Promise<any>;
|
|
49
|
+
export declare function createAppInfoLocalization(appId: string, locale: string, fields: AppInfoLocalizationFields): Promise<{
|
|
50
|
+
appInfoId: string;
|
|
51
|
+
appInfoState: string;
|
|
52
|
+
localization: {
|
|
53
|
+
id: any;
|
|
54
|
+
locale: any;
|
|
55
|
+
name: any;
|
|
56
|
+
subtitle: any;
|
|
57
|
+
privacyPolicyUrl: any;
|
|
58
|
+
privacyPolicyText: any;
|
|
59
|
+
};
|
|
60
|
+
}>;
|
|
61
|
+
export interface ListCustomerReviewsOptions {
|
|
62
|
+
limit?: number;
|
|
63
|
+
territory?: string;
|
|
64
|
+
rating?: 1 | 2 | 3 | 4 | 5;
|
|
65
|
+
}
|
|
66
|
+
export declare function listCustomerReviews(appId: string, opts?: ListCustomerReviewsOptions): Promise<{
|
|
67
|
+
id: string;
|
|
68
|
+
rating: number | undefined;
|
|
69
|
+
title: string | undefined;
|
|
70
|
+
body: string | undefined;
|
|
71
|
+
nickname: string | undefined;
|
|
72
|
+
createdDate: string | undefined;
|
|
73
|
+
territory: string | undefined;
|
|
74
|
+
response: {
|
|
75
|
+
body: string | undefined;
|
|
76
|
+
lastModifiedDate: string | undefined;
|
|
77
|
+
state: string | undefined;
|
|
78
|
+
} | null;
|
|
79
|
+
}[]>;
|
|
80
|
+
export declare function createReviewResponse(reviewId: string, responseBody: string): Promise<any>;
|
|
81
|
+
export {};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// App Store Connect — 앱·앱 정보(로컬라이제이션)·TestFlight 베타 그룹·고객 리뷰.
|
|
2
|
+
// appstore/tools.ts 가 이 모듈을 그대로 re-export 한다 — 호출부는 tools.js 경로를 계속 쓴다.
|
|
3
|
+
import { apiGet, apiPatch, apiPost } from './client.js';
|
|
4
|
+
import { encodePathSegment } from '../lib/url-path.js';
|
|
5
|
+
export async function listApps() {
|
|
6
|
+
const data = (await apiGet('/apps', {
|
|
7
|
+
'fields[apps]': 'name,bundleId,sku,primaryLocale,contentRightsDeclaration',
|
|
8
|
+
'limit': '200',
|
|
9
|
+
}));
|
|
10
|
+
return (data.data ?? []).map((a) => ({
|
|
11
|
+
id: a.id,
|
|
12
|
+
name: a.attributes?.name,
|
|
13
|
+
bundleId: a.attributes?.bundleId,
|
|
14
|
+
sku: a.attributes?.sku,
|
|
15
|
+
primaryLocale: a.attributes?.primaryLocale,
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
export async function getApp(appId) {
|
|
19
|
+
const data = (await apiGet(`/apps/${encodePathSegment(appId)}`, {
|
|
20
|
+
'fields[apps]': 'name,bundleId,sku,primaryLocale,contentRightsDeclaration',
|
|
21
|
+
'include': 'appStoreVersions',
|
|
22
|
+
}));
|
|
23
|
+
return data.data;
|
|
24
|
+
}
|
|
25
|
+
// ─── TestFlight 베타 그룹 ───
|
|
26
|
+
export async function listBetaGroups(appId) {
|
|
27
|
+
const data = (await apiGet(`/apps/${encodePathSegment(appId)}/betaGroups`, {
|
|
28
|
+
'fields[betaGroups]': 'name,isInternalGroup,publicLink,publicLinkEnabled',
|
|
29
|
+
}));
|
|
30
|
+
return (data.data ?? []).map((g) => ({
|
|
31
|
+
id: g.id,
|
|
32
|
+
name: g.attributes?.name,
|
|
33
|
+
isInternal: g.attributes?.isInternalGroup,
|
|
34
|
+
publicLink: g.attributes?.publicLink,
|
|
35
|
+
publicLinkEnabled: g.attributes?.publicLinkEnabled,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
// ─── 앱 정보 (카테고리 등) ───
|
|
39
|
+
// AppInfoState — READY_FOR_DISTRIBUTION이 라이브 버전, 그 외(PREPARE_FOR_SUBMISSION /
|
|
40
|
+
// DEVELOPER_REJECTED 등)가 편집 가능한 appInfo.
|
|
41
|
+
const APP_INFO_LIVE_STATE = 'READY_FOR_DISTRIBUTION';
|
|
42
|
+
export async function getAppInfo(appId) {
|
|
43
|
+
const data = (await apiGet(`/apps/${encodePathSegment(appId)}/appInfos`, {
|
|
44
|
+
'fields[appInfos]': 'state,appStoreAgeRating,brazilAgeRating',
|
|
45
|
+
}));
|
|
46
|
+
return (data.data ?? []).map((i) => ({
|
|
47
|
+
id: i.id,
|
|
48
|
+
// 새 필드명 state, 옛 필드명 appStoreState 모두 케어
|
|
49
|
+
state: i.attributes?.state ?? i.attributes?.appStoreState,
|
|
50
|
+
ageRating: i.attributes?.appStoreAgeRating,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
async function findEditableAppInfoId(appId) {
|
|
54
|
+
const data = await apiGet(`/apps/${encodePathSegment(appId)}/appInfos`, {
|
|
55
|
+
'fields[appInfos]': 'state',
|
|
56
|
+
'limit': '10',
|
|
57
|
+
});
|
|
58
|
+
const infos = (data?.data ?? []);
|
|
59
|
+
if (infos.length === 0) {
|
|
60
|
+
throw new Error(`앱 ${appId}에 appInfos가 없어. 앱 ID 확인 필요.`);
|
|
61
|
+
}
|
|
62
|
+
const stateOf = (i) => i.attributes?.state ?? i.attributes?.appStoreState ?? '';
|
|
63
|
+
const editable = infos.find((i) => stateOf(i) !== APP_INFO_LIVE_STATE);
|
|
64
|
+
const target = editable ?? infos[0];
|
|
65
|
+
return { appInfoId: target.id, state: stateOf(target) };
|
|
66
|
+
}
|
|
67
|
+
export async function listAppInfoLocalizations(appId, locale) {
|
|
68
|
+
const { appInfoId, state } = await findEditableAppInfoId(appId);
|
|
69
|
+
const data = await apiGet(`/appInfos/${encodePathSegment(appInfoId)}/appInfoLocalizations`, {
|
|
70
|
+
'fields[appInfoLocalizations]': 'locale,name,subtitle,privacyPolicyUrl,privacyPolicyText',
|
|
71
|
+
'limit': '200',
|
|
72
|
+
});
|
|
73
|
+
const all = (data?.data ?? []).map((l) => ({
|
|
74
|
+
id: l.id,
|
|
75
|
+
locale: l.attributes?.locale,
|
|
76
|
+
name: l.attributes?.name,
|
|
77
|
+
subtitle: l.attributes?.subtitle,
|
|
78
|
+
privacyPolicyUrl: l.attributes?.privacyPolicyUrl,
|
|
79
|
+
privacyPolicyText: l.attributes?.privacyPolicyText,
|
|
80
|
+
}));
|
|
81
|
+
const filtered = locale ? all.filter((l) => l.locale === locale) : all;
|
|
82
|
+
return { appInfoId, appInfoState: state, localizations: filtered };
|
|
83
|
+
}
|
|
84
|
+
export async function updateAppInfoLocalization(localizationId, fields) {
|
|
85
|
+
const attributes = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
|
|
86
|
+
if (Object.keys(attributes).length === 0) {
|
|
87
|
+
throw new Error('수정할 필드가 없어 (name / subtitle / privacyPolicyUrl / privacyPolicyText 중 하나 이상).');
|
|
88
|
+
}
|
|
89
|
+
return apiPatch(`/appInfoLocalizations/${encodePathSegment(localizationId)}`, {
|
|
90
|
+
data: {
|
|
91
|
+
type: 'appInfoLocalizations',
|
|
92
|
+
id: localizationId,
|
|
93
|
+
attributes,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export async function createAppInfoLocalization(appId, locale, fields) {
|
|
98
|
+
const attributes = Object.fromEntries(Object.entries(fields).filter(([, v]) => v !== undefined));
|
|
99
|
+
const { appInfoId, state } = await findEditableAppInfoId(appId);
|
|
100
|
+
const data = await apiPost('/appInfoLocalizations', {
|
|
101
|
+
data: {
|
|
102
|
+
type: 'appInfoLocalizations',
|
|
103
|
+
attributes: { locale, ...attributes },
|
|
104
|
+
relationships: {
|
|
105
|
+
appInfo: { data: { type: 'appInfos', id: appInfoId } },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
const created = data?.data;
|
|
110
|
+
return {
|
|
111
|
+
appInfoId,
|
|
112
|
+
appInfoState: state,
|
|
113
|
+
localization: {
|
|
114
|
+
id: created?.id,
|
|
115
|
+
locale: created?.attributes?.locale,
|
|
116
|
+
name: created?.attributes?.name,
|
|
117
|
+
subtitle: created?.attributes?.subtitle,
|
|
118
|
+
privacyPolicyUrl: created?.attributes?.privacyPolicyUrl,
|
|
119
|
+
privacyPolicyText: created?.attributes?.privacyPolicyText,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export async function listCustomerReviews(appId, opts = {}) {
|
|
124
|
+
const params = {
|
|
125
|
+
'sort': '-createdDate',
|
|
126
|
+
'limit': String(opts.limit ?? 50),
|
|
127
|
+
'fields[customerReviews]': 'rating,title,body,reviewerNickname,createdDate,territory',
|
|
128
|
+
'include': 'response',
|
|
129
|
+
'fields[customerReviewResponses]': 'responseBody,lastModifiedDate,state',
|
|
130
|
+
};
|
|
131
|
+
if (opts.territory)
|
|
132
|
+
params['filter[territory]'] = opts.territory;
|
|
133
|
+
if (opts.rating != null)
|
|
134
|
+
params['filter[rating]'] = String(opts.rating);
|
|
135
|
+
const data = (await apiGet(`/apps/${encodePathSegment(appId)}/customerReviews`, params));
|
|
136
|
+
// include로 가져온 답변 매핑
|
|
137
|
+
const responses = new Map();
|
|
138
|
+
for (const inc of data.included ?? []) {
|
|
139
|
+
if (inc.type === 'customerReviewResponses') {
|
|
140
|
+
responses.set(inc.id, inc.attributes);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return (data.data ?? []).map((r) => {
|
|
144
|
+
const respId = r.relationships?.response?.data?.id;
|
|
145
|
+
const resp = respId ? responses.get(respId) : null;
|
|
146
|
+
return {
|
|
147
|
+
id: r.id,
|
|
148
|
+
rating: r.attributes?.rating,
|
|
149
|
+
title: r.attributes?.title,
|
|
150
|
+
body: r.attributes?.body,
|
|
151
|
+
nickname: r.attributes?.reviewerNickname,
|
|
152
|
+
createdDate: r.attributes?.createdDate,
|
|
153
|
+
territory: r.attributes?.territory,
|
|
154
|
+
response: resp
|
|
155
|
+
? {
|
|
156
|
+
body: resp.responseBody,
|
|
157
|
+
lastModifiedDate: resp.lastModifiedDate,
|
|
158
|
+
state: resp.state,
|
|
159
|
+
}
|
|
160
|
+
: null,
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
export async function createReviewResponse(reviewId, responseBody) {
|
|
165
|
+
return apiPost('/customerReviewResponses', {
|
|
166
|
+
data: {
|
|
167
|
+
type: 'customerReviewResponses',
|
|
168
|
+
attributes: { responseBody },
|
|
169
|
+
relationships: {
|
|
170
|
+
review: { data: { type: 'customerReviews', id: reviewId } },
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare function apiGet(path: string, params?: Record<string, string>): Promise<any>;
|
|
2
|
+
export interface AppStoreVerifyResult {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
stage: 'creds' | 'sign' | 'auth' | 'api' | 'done';
|
|
5
|
+
message: string;
|
|
6
|
+
httpStatus?: number;
|
|
7
|
+
appCount?: number;
|
|
8
|
+
firstApp?: {
|
|
9
|
+
id: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* appstore.json 자격증명 유효성 단계별 검증 (creds → sign → auth → api).
|
|
15
|
+
* playstore_verify_service_account 의 App Store 대응. 읽기 전용 — GET /apps?limit=1.
|
|
16
|
+
* 파일 존재만 보는 requireAppStoreCreds 와 달리, 잘못된 .p8/keyId/issuerId 를
|
|
17
|
+
* "첫 호출 401" 로 늦게 터지기 전에 setup 단계에서 잡아준다.
|
|
18
|
+
*/
|
|
19
|
+
export declare function verifyAppStoreCredentials(creds?: import("./auth.js").AppStoreCredentials | null): Promise<AppStoreVerifyResult>;
|
|
20
|
+
export declare function apiPatch(path: string, body: unknown): Promise<any>;
|
|
21
|
+
export declare function apiPost(path: string, body: unknown): Promise<any>;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { getAuthHeaders, getAppStoreCredentials, generateToken } from './auth.js';
|
|
2
|
+
import { friendlyAppStoreError } from './errors.js';
|
|
3
|
+
import { fetchWithTimeout } from '../lib/http.js';
|
|
4
|
+
/**
|
|
5
|
+
* App Store Connect API v1 래퍼
|
|
6
|
+
* https://developer.apple.com/documentation/appstoreconnectapi
|
|
7
|
+
*/
|
|
8
|
+
const BASE = 'https://api.appstoreconnect.apple.com/v1';
|
|
9
|
+
export async function apiGet(path, params) {
|
|
10
|
+
const headers = await getAuthHeaders();
|
|
11
|
+
if (!headers)
|
|
12
|
+
throw new Error([
|
|
13
|
+
'❌ App Store Connect 인증이 필요해.',
|
|
14
|
+
'',
|
|
15
|
+
'터미널에서 실행:',
|
|
16
|
+
' npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
|
|
17
|
+
'',
|
|
18
|
+
'API Key가 필요해:',
|
|
19
|
+
' App Store Connect > Users and Access > Integrations > Keys',
|
|
20
|
+
].join('\n'));
|
|
21
|
+
const url = new URL(`${BASE}${path}`);
|
|
22
|
+
if (params) {
|
|
23
|
+
for (const [k, v] of Object.entries(params))
|
|
24
|
+
url.searchParams.set(k, v);
|
|
25
|
+
}
|
|
26
|
+
const res = await fetchWithTimeout(url.toString(), { headers });
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const body = await res.text();
|
|
29
|
+
throw friendlyAppStoreError(res.status, body);
|
|
30
|
+
}
|
|
31
|
+
return res.json();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* appstore.json 자격증명 유효성 단계별 검증 (creds → sign → auth → api).
|
|
35
|
+
* playstore_verify_service_account 의 App Store 대응. 읽기 전용 — GET /apps?limit=1.
|
|
36
|
+
* 파일 존재만 보는 requireAppStoreCreds 와 달리, 잘못된 .p8/keyId/issuerId 를
|
|
37
|
+
* "첫 호출 401" 로 늦게 터지기 전에 setup 단계에서 잡아준다.
|
|
38
|
+
*/
|
|
39
|
+
export async function verifyAppStoreCredentials(creds = getAppStoreCredentials()) {
|
|
40
|
+
if (!creds) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
stage: 'creds',
|
|
44
|
+
message: '~/.mimi-seed/appstore.json 이 없습니다. `mimi-seed auth appstore` 로 등록하세요.',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
let token;
|
|
48
|
+
try {
|
|
49
|
+
token = await generateToken(creds);
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
stage: 'sign',
|
|
55
|
+
message: `JWT 서명 실패 — .p8 privateKey / keyId 확인 필요.\n${e.message}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
let res;
|
|
59
|
+
try {
|
|
60
|
+
res = await fetchWithTimeout(`${BASE}/apps?limit=1`, { headers: { Authorization: `Bearer ${token}` } });
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return { ok: false, stage: 'api', message: `App Store API 연결 실패: ${e.message}` };
|
|
64
|
+
}
|
|
65
|
+
if (res.status === 401 || res.status === 403) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
stage: 'auth',
|
|
69
|
+
httpStatus: res.status,
|
|
70
|
+
message: 'Apple이 인증을 거부했어요 — issuerId / keyId / .p8 조합이 틀렸거나 키 권한이 부족합니다.\n`mimi-seed auth appstore` 로 재등록하세요.',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (!res.ok) {
|
|
74
|
+
const body = await res.text();
|
|
75
|
+
return { ok: false, stage: 'api', httpStatus: res.status, message: `App Store API ${res.status}: ${body.slice(0, 200)}` };
|
|
76
|
+
}
|
|
77
|
+
const data = (await res.json());
|
|
78
|
+
const first = data.data?.[0];
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
stage: 'done',
|
|
82
|
+
message: '인증 유효',
|
|
83
|
+
appCount: data.meta?.paging?.total,
|
|
84
|
+
firstApp: first ? { id: first.id, name: first.attributes?.name } : undefined,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export async function apiPatch(path, body) {
|
|
88
|
+
const headers = await getAuthHeaders();
|
|
89
|
+
if (!headers)
|
|
90
|
+
throw new Error([
|
|
91
|
+
'❌ App Store Connect 인증이 필요해.',
|
|
92
|
+
'',
|
|
93
|
+
'터미널에서 실행:',
|
|
94
|
+
' npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
|
|
95
|
+
].join('\n'));
|
|
96
|
+
const res = await fetchWithTimeout(`${BASE}${path}`, {
|
|
97
|
+
method: 'PATCH',
|
|
98
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
99
|
+
body: JSON.stringify(body),
|
|
100
|
+
});
|
|
101
|
+
if (!res.ok) {
|
|
102
|
+
const text = await res.text();
|
|
103
|
+
throw friendlyAppStoreError(res.status, text);
|
|
104
|
+
}
|
|
105
|
+
// 204 No Content 가능
|
|
106
|
+
const text = await res.text();
|
|
107
|
+
return text ? JSON.parse(text) : { ok: true };
|
|
108
|
+
}
|
|
109
|
+
export async function apiPost(path, body) {
|
|
110
|
+
const headers = await getAuthHeaders();
|
|
111
|
+
if (!headers)
|
|
112
|
+
throw new Error([
|
|
113
|
+
'❌ App Store Connect 인증이 필요해.',
|
|
114
|
+
'',
|
|
115
|
+
'터미널에서 실행:',
|
|
116
|
+
' npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth',
|
|
117
|
+
].join('\n'));
|
|
118
|
+
const res = await fetchWithTimeout(`${BASE}${path}`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
121
|
+
body: JSON.stringify(body),
|
|
122
|
+
});
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
throw friendlyAppStoreError(res.status, text);
|
|
126
|
+
}
|
|
127
|
+
// 201 Created — 본문에 created entity. 일부 엔드포인트는 204
|
|
128
|
+
const text = await res.text();
|
|
129
|
+
return text ? JSON.parse(text) : { ok: true };
|
|
130
|
+
}
|