@yoonion/mimi-seed-mcp 0.3.13 → 0.3.14

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.
@@ -28,13 +28,36 @@ export function saveAppStoreCredentials(creds) {
28
28
  fs.mkdirSync(dir, { recursive: true });
29
29
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(creds, null, 2), { mode: 0o600 });
30
30
  }
31
+ function normalizePrivateKey(raw) {
32
+ // Normalize CRLF → LF, strip extra whitespace from lines
33
+ const lines = raw.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
34
+ const header = lines.find(l => l.startsWith('-----BEGIN'));
35
+ const footer = lines.find(l => l.startsWith('-----END'));
36
+ if (!header || !footer)
37
+ return raw; // not PEM, pass through and let importPKCS8 error
38
+ const body = lines
39
+ .filter(l => l && !l.startsWith('-----'))
40
+ .join('');
41
+ // Re-chunk into 64-char lines (standard PEM)
42
+ const chunks = body.match(/.{1,64}/g) ?? [];
43
+ return [header, ...chunks, footer, ''].join('\n');
44
+ }
31
45
  export async function generateToken(creds) {
32
- const key = await importPKCS8(creds.privateKey, 'ES256');
46
+ const normalizedKey = normalizePrivateKey(creds.privateKey);
47
+ let key;
48
+ try {
49
+ key = await importPKCS8(normalizedKey, 'ES256');
50
+ }
51
+ catch (err) {
52
+ throw new Error(`App Store 개인 키 파싱 실패 — ~/.mimi-seed/appstore.json의 privateKey 형식 확인 필요.\n원인: ${err.message}`);
53
+ }
54
+ // Subtract 60s from iat to tolerate local clock running slightly ahead of Apple servers.
55
+ const iat = Math.floor(Date.now() / 1000) - 60;
33
56
  return new SignJWT({})
34
57
  .setProtectedHeader({ alg: 'ES256', kid: creds.keyId, typ: 'JWT' })
35
58
  .setIssuer(creds.issuerId)
36
- .setIssuedAt()
37
- .setExpirationTime('20m')
59
+ .setIssuedAt(iat)
60
+ .setExpirationTime(iat + 20 * 60)
38
61
  .setAudience('appstoreconnect-v1')
39
62
  .sign(key);
40
63
  }
@@ -41,6 +41,15 @@ export declare function updateVersionLocalization(localizationId: string, fields
41
41
  * localizationId를 직접 모를 때 편의용.
42
42
  */
43
43
  export declare function updateVersionWhatsNew(versionId: string, locale: string, fields: LocalizationUpdateFields): Promise<any>;
44
+ export declare function updateReviewNotes(versionId: string, notes: string): Promise<{
45
+ reviewDetailId: string;
46
+ notes: string;
47
+ }>;
48
+ export declare function getReviewNotes(versionId: string): Promise<{
49
+ reviewDetailId: string | null;
50
+ notes: string | null;
51
+ contactEmail: string | null;
52
+ }>;
44
53
  export declare function listBuilds(appId: string): Promise<any>;
45
54
  export declare function listBetaGroups(appId: string): Promise<any>;
46
55
  export declare function getAppInfo(appId: string): Promise<any>;
@@ -185,6 +185,60 @@ export async function updateVersionWhatsNew(versionId, locale, fields) {
185
185
  }
186
186
  return updateVersionLocalization(target.id, fields);
187
187
  }
188
+ // ─── 리뷰어 노트 (appStoreReviewDetail.notes) ───
189
+ export async function updateReviewNotes(versionId, notes) {
190
+ // 1. 기존 reviewDetail 조회
191
+ let reviewDetailId = null;
192
+ try {
193
+ const existing = await apiGet(`/appStoreVersions/${versionId}/appStoreReviewDetail`, {
194
+ 'fields[appStoreReviewDetails]': 'notes,contactFirstName,contactLastName,contactPhone,contactEmail',
195
+ });
196
+ reviewDetailId = existing?.data?.id ?? null;
197
+ }
198
+ catch {
199
+ // 없으면 새로 생성
200
+ }
201
+ if (reviewDetailId) {
202
+ // 2a. 있으면 PATCH
203
+ const updated = await apiPatch(`/appStoreReviewDetails/${reviewDetailId}`, {
204
+ data: {
205
+ type: 'appStoreReviewDetails',
206
+ id: reviewDetailId,
207
+ attributes: { notes },
208
+ },
209
+ });
210
+ return { reviewDetailId, notes: updated?.data?.attributes?.notes ?? notes };
211
+ }
212
+ else {
213
+ // 2b. 없으면 POST (version과 relationship 연결)
214
+ const created = await apiPost('/appStoreReviewDetails', {
215
+ data: {
216
+ type: 'appStoreReviewDetails',
217
+ attributes: { notes },
218
+ relationships: {
219
+ appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
220
+ },
221
+ },
222
+ });
223
+ const newId = created?.data?.id ?? '';
224
+ return { reviewDetailId: newId, notes: created?.data?.attributes?.notes ?? notes };
225
+ }
226
+ }
227
+ export async function getReviewNotes(versionId) {
228
+ try {
229
+ const data = await apiGet(`/appStoreVersions/${versionId}/appStoreReviewDetail`, {
230
+ 'fields[appStoreReviewDetails]': 'notes,contactEmail,demoAccountName,demoAccountRequired',
231
+ });
232
+ return {
233
+ reviewDetailId: data?.data?.id ?? null,
234
+ notes: data?.data?.attributes?.notes ?? null,
235
+ contactEmail: data?.data?.attributes?.contactEmail ?? null,
236
+ };
237
+ }
238
+ catch {
239
+ return { reviewDetailId: null, notes: null, contactEmail: null };
240
+ }
241
+ }
188
242
  // ─── 빌드 ───
189
243
  export async function listBuilds(appId) {
190
244
  const data = await apiGet(`/builds`, {
package/dist/index.js CHANGED
@@ -830,6 +830,32 @@ server.tool('appstore_update_whats_new', "App Store '이 버전의 새로운 기
830
830
  const result = await appstore.updateVersionWhatsNew(versionId, locale, { whatsNew });
831
831
  return { content: [{ type: 'text', text: `✅ ${locale} 로캘의 What's New가 업데이트됐어.\n\n${JSON.stringify(result, null, 2)}` }] };
832
832
  });
833
+ server.tool('appstore_update_review_notes', "App Store 심사 리뷰어 노트(Notes for App Review) 등록/수정. versionId 버전에 appStoreReviewDetail.notes를 PATCH하거나 없으면 POST로 생성. 심사 시 리뷰어에게 전달되는 테스트 계정·기능 안내 텍스트 작성에 사용.", {
834
+ versionId: z.string().describe('버전 ID (appstore_list_versions 결과)'),
835
+ notes: z.string().describe('리뷰어에게 전달할 메모 (테스트 계정, 주요 변경사항, 접근 방법 등). 4000자 이내 권장.'),
836
+ }, async ({ versionId, notes }) => {
837
+ const result = await appstore.updateReviewNotes(versionId, notes);
838
+ return {
839
+ content: [{
840
+ type: 'text',
841
+ text: `✅ 리뷰어 노트가 저장됐어.\n\nreviewDetailId: ${result.reviewDetailId}\n\n${result.notes}`,
842
+ }],
843
+ };
844
+ });
845
+ server.tool('appstore_get_review_notes', "App Store 심사 리뷰어 노트(Notes for App Review) 조회. 현재 등록된 notes, contactEmail 확인용.", {
846
+ versionId: z.string().describe('버전 ID (appstore_list_versions 결과)'),
847
+ }, async ({ versionId }) => {
848
+ const result = await appstore.getReviewNotes(versionId);
849
+ if (!result.reviewDetailId) {
850
+ return { content: [{ type: 'text', text: '이 버전에는 아직 리뷰어 노트가 없어. appstore_update_review_notes로 등록해줘.' }] };
851
+ }
852
+ return {
853
+ content: [{
854
+ type: 'text',
855
+ text: `reviewDetailId: ${result.reviewDetailId}\ncontactEmail: ${result.contactEmail ?? '(없음)'}\n\n노트:\n${result.notes ?? '(비어있음)'}`,
856
+ }],
857
+ };
858
+ });
833
859
  server.tool('appstore_list_builds', 'TestFlight 빌드 목록', { appId: z.string().describe('앱 ID') }, async ({ appId }) => {
834
860
  const builds = await appstore.listBuilds(appId);
835
861
  return { content: [{ type: 'text', text: JSON.stringify(builds, null, 2) }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {