@yoonion/mimi-seed-mcp 0.3.15 → 0.3.16
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/checks/risks.js +65 -11
- package/package.json +1 -1
package/dist/checks/risks.js
CHANGED
|
@@ -53,21 +53,42 @@ export async function checkPlayStoreRisks(auth, packageName, language = 'ko-KR')
|
|
|
53
53
|
});
|
|
54
54
|
return risks;
|
|
55
55
|
}
|
|
56
|
+
// Apple의 ASC API 호출은 권한·필드명·deprecated endpoint 등으로 실패할 수 있다.
|
|
57
|
+
// 실패를 silent하게 null로 만들면 후속 check가 false-positive blocker를 토해낸다 —
|
|
58
|
+
// 이 헬퍼는 실패 사실을 risks 배열에 warning으로 기록해 사용자에게 노출시키되,
|
|
59
|
+
// 호출부는 null을 받아 blocker 분기를 건너뛸 수 있게 해준다.
|
|
60
|
+
async function safeGet(call, risks, code, title) {
|
|
61
|
+
try {
|
|
62
|
+
return await call();
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
risks.push({
|
|
66
|
+
level: 'warning',
|
|
67
|
+
code: `API_ERROR_${code}`,
|
|
68
|
+
title: `App Store Connect 조회 실패 — ${title}`,
|
|
69
|
+
detail: `이 항목은 위험 분석에서 제외됨. 원인: ${e instanceof Error ? e.message : String(e)}`,
|
|
70
|
+
});
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const APP_INFO_LIVE_STATE = 'READY_FOR_DISTRIBUTION';
|
|
56
75
|
export async function checkAppStoreRisks(appId) {
|
|
57
76
|
const risks = [];
|
|
58
|
-
const versions = await apiGet(`/apps/${appId}/appStoreVersions`, {
|
|
77
|
+
const versions = await safeGet(() => apiGet(`/apps/${appId}/appStoreVersions`, {
|
|
59
78
|
'filter[appStoreState]': APPSTORE_EDITABLE_STATES,
|
|
60
79
|
'limit': '5',
|
|
61
|
-
})
|
|
80
|
+
}), risks, 'VERSIONS', '편집 가능한 버전 목록');
|
|
62
81
|
if (!versions?.data?.length) {
|
|
63
82
|
risks.push({ level: 'warning', code: 'NO_EDITABLE_VERSION', title: '편집 가능한 버전 없음', detail: 'App Store Connect에서 새 버전을 만드세요.', fixUrl: 'https://appstoreconnect.apple.com' });
|
|
64
83
|
return risks;
|
|
65
84
|
}
|
|
66
85
|
const versionId = versions.data[0].id;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
apiGet(`/
|
|
86
|
+
// 1) 메타데이터·스크린샷·버전 첨부 빌드는 versionId 기반으로 조회.
|
|
87
|
+
// 2) 개인정보 URL은 appInfoLocalization 단위(앱 단위)로 조회.
|
|
88
|
+
const [locs, attachedBuildRel, appInfos] = await Promise.all([
|
|
89
|
+
safeGet(() => apiGet(`/appStoreVersions/${versionId}/appStoreVersionLocalizations`), risks, 'LOCALIZATIONS', '버전 로컬라이제이션'),
|
|
90
|
+
safeGet(() => apiGet(`/appStoreVersions/${versionId}/relationships/build`), risks, 'BUILD', '버전 첨부 빌드'),
|
|
91
|
+
safeGet(() => apiGet(`/apps/${appId}/appInfos`, { 'fields[appInfos]': 'state', 'limit': '10' }), risks, 'APP_INFO', '앱 정보'),
|
|
71
92
|
]);
|
|
72
93
|
if (!locs?.data?.length) {
|
|
73
94
|
risks.push({ level: 'blocker', code: 'NO_LOCALIZATIONS', title: '로컬라이제이션 없음', detail: '메타데이터를 입력하세요.' });
|
|
@@ -84,16 +105,49 @@ export async function checkAppStoreRisks(appId) {
|
|
|
84
105
|
}
|
|
85
106
|
// screenshots depend on locs being present
|
|
86
107
|
const locId = locs.data[0].id;
|
|
87
|
-
const screenshots = await apiGet(`/appStoreVersionLocalizations/${locId}/appScreenshotSets`)
|
|
88
|
-
if (!screenshots?.data?.length) {
|
|
108
|
+
const screenshots = await safeGet(() => apiGet(`/appStoreVersionLocalizations/${locId}/appScreenshotSets`), risks, 'SCREENSHOTS', '스크린샷 셋');
|
|
109
|
+
if (screenshots && !screenshots?.data?.length) {
|
|
89
110
|
risks.push({ level: 'blocker', code: 'NO_SCREENSHOTS', title: '스크린샷 없음', detail: 'iPhone 6.5" 또는 6.9" 스크린샷이 필요합니다.' });
|
|
90
111
|
}
|
|
91
112
|
}
|
|
92
|
-
|
|
113
|
+
// 빌드 — 1차: 버전에 첨부된 빌드. 2차 폴백: 앱 전체 빌드 (예전 동작).
|
|
114
|
+
// attachedBuildRel?.data가 null이면 "관계 조회 성공 + 첨부 안 됨" 의미.
|
|
115
|
+
// attachedBuildRel 자체가 null이면 safeGet 실패 — fall back.
|
|
116
|
+
let buildVerdict;
|
|
117
|
+
if (attachedBuildRel) {
|
|
118
|
+
buildVerdict = attachedBuildRel.data ? 'present' : 'missing';
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const fallbackBuilds = await safeGet(() => apiGet(`/builds`, {
|
|
122
|
+
'filter[app]': appId,
|
|
123
|
+
'fields[builds]': 'version,uploadedDate,processingState',
|
|
124
|
+
'sort': '-uploadedDate',
|
|
125
|
+
'limit': '5',
|
|
126
|
+
}), risks, 'BUILDS_FALLBACK', '앱 빌드 목록(폴백)');
|
|
127
|
+
buildVerdict = fallbackBuilds?.data?.length ? 'present' : fallbackBuilds ? 'missing' : 'unknown';
|
|
128
|
+
}
|
|
129
|
+
if (buildVerdict === 'missing') {
|
|
93
130
|
risks.push({ level: 'blocker', code: 'NO_BUILD', title: 'TestFlight 빌드 없음', detail: 'Xcode 또는 Fastlane으로 빌드를 업로드하세요.' });
|
|
94
131
|
}
|
|
95
|
-
|
|
96
|
-
|
|
132
|
+
// 개인정보처리방침 — appInfoLocalization 어디 한 곳이라도 URL 또는 in-app 텍스트가 있으면 OK.
|
|
133
|
+
// appInfos 조회 자체가 실패하면 unknown으로 두고 blocker 안 띄움 (warning만 기록됨).
|
|
134
|
+
if (appInfos) {
|
|
135
|
+
const infos = (appInfos.data ?? []);
|
|
136
|
+
const stateOf = (i) => i.attributes?.state ?? i.attributes?.appStoreState ?? '';
|
|
137
|
+
const editable = infos.find((i) => stateOf(i) !== APP_INFO_LIVE_STATE) ?? infos[0];
|
|
138
|
+
if (editable) {
|
|
139
|
+
const localizations = await safeGet(() => apiGet(`/appInfos/${editable.id}/appInfoLocalizations`, {
|
|
140
|
+
'fields[appInfoLocalizations]': 'locale,privacyPolicyUrl,privacyPolicyText',
|
|
141
|
+
'limit': '200',
|
|
142
|
+
}), risks, 'PRIVACY', '개인정보 로컬라이제이션');
|
|
143
|
+
if (localizations) {
|
|
144
|
+
const locs = (localizations.data ?? []);
|
|
145
|
+
const hasPrivacy = locs.some((l) => l.attributes?.privacyPolicyUrl || l.attributes?.privacyPolicyText);
|
|
146
|
+
if (!hasPrivacy) {
|
|
147
|
+
risks.push({ level: 'blocker', code: 'NO_PRIVACY', title: '개인정보처리방침 URL 없음', detail: 'App Store 가이드라인 5.1.1 — 필수 항목입니다. 모든 로컬라이제이션에서 privacyPolicyUrl 또는 privacyPolicyText 중 하나를 입력하세요.' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
97
151
|
}
|
|
98
152
|
return risks;
|
|
99
153
|
}
|
package/package.json
CHANGED