@yoonion/mimi-seed-mcp 0.3.12 → 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.
@@ -5,5 +5,5 @@ export interface AppStoreCredentials {
5
5
  }
6
6
  export declare function getAppStoreCredentials(): AppStoreCredentials | null;
7
7
  export declare function saveAppStoreCredentials(creds: AppStoreCredentials): void;
8
- export declare function generateToken(creds: AppStoreCredentials): string;
9
- export declare function getAuthHeaders(): Record<string, string> | null;
8
+ export declare function generateToken(creds: AppStoreCredentials): Promise<string>;
9
+ export declare function getAuthHeaders(): Promise<Record<string, string> | null>;
@@ -1,4 +1,4 @@
1
- import jwt from 'jsonwebtoken';
1
+ import { SignJWT, importPKCS8 } from 'jose';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import os from 'node:os';
@@ -28,26 +28,43 @@ 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
- export function generateToken(creds) {
32
- const now = Math.floor(Date.now() / 1000);
33
- return jwt.sign({
34
- iss: creds.issuerId,
35
- iat: now,
36
- exp: now + 20 * 60, // 20분
37
- aud: 'appstoreconnect-v1',
38
- }, creds.privateKey, {
39
- algorithm: 'ES256',
40
- header: {
41
- alg: 'ES256',
42
- kid: creds.keyId,
43
- typ: 'JWT',
44
- },
45
- });
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');
46
44
  }
47
- export function getAuthHeaders() {
45
+ export async function generateToken(creds) {
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;
56
+ return new SignJWT({})
57
+ .setProtectedHeader({ alg: 'ES256', kid: creds.keyId, typ: 'JWT' })
58
+ .setIssuer(creds.issuerId)
59
+ .setIssuedAt(iat)
60
+ .setExpirationTime(iat + 20 * 60)
61
+ .setAudience('appstoreconnect-v1')
62
+ .sign(key);
63
+ }
64
+ export async function getAuthHeaders() {
48
65
  const creds = getAppStoreCredentials();
49
66
  if (!creds)
50
67
  return null;
51
- const token = generateToken(creds);
68
+ const token = await generateToken(creds);
52
69
  return { Authorization: `Bearer ${token}` };
53
70
  }
@@ -14,8 +14,8 @@ import crypto from 'node:crypto';
14
14
  * 4. commit — PATCH /appScreenshots/{id} { uploaded: true, sourceFileChecksum }
15
15
  */
16
16
  const BASE = 'https://api.appstoreconnect.apple.com/v1';
17
- function authHeadersOrThrow() {
18
- const headers = getAuthHeaders();
17
+ async function authHeadersOrThrow() {
18
+ const headers = await getAuthHeaders();
19
19
  if (!headers) {
20
20
  throw new Error([
21
21
  '❌ App Store Connect 인증이 필요해.',
@@ -27,7 +27,7 @@ function authHeadersOrThrow() {
27
27
  return headers;
28
28
  }
29
29
  async function req(pathOrUrl, init = {}) {
30
- const headers = authHeadersOrThrow();
30
+ const headers = await authHeadersOrThrow();
31
31
  const url = pathOrUrl.startsWith('http') ? pathOrUrl : `${BASE}${pathOrUrl}`;
32
32
  const res = await fetch(url, {
33
33
  ...init,
@@ -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>;
@@ -5,7 +5,7 @@ import { getAuthHeaders } from './auth.js';
5
5
  */
6
6
  const BASE = 'https://api.appstoreconnect.apple.com/v1';
7
7
  export async function apiGet(path, params) {
8
- const headers = getAuthHeaders();
8
+ const headers = await getAuthHeaders();
9
9
  if (!headers)
10
10
  throw new Error([
11
11
  '❌ App Store Connect 인증이 필요해.',
@@ -29,7 +29,7 @@ export async function apiGet(path, params) {
29
29
  return res.json();
30
30
  }
31
31
  async function apiPatch(path, body) {
32
- const headers = getAuthHeaders();
32
+ const headers = await getAuthHeaders();
33
33
  if (!headers)
34
34
  throw new Error([
35
35
  '❌ App Store Connect 인증이 필요해.',
@@ -51,7 +51,7 @@ async function apiPatch(path, body) {
51
51
  return text ? JSON.parse(text) : { ok: true };
52
52
  }
53
53
  async function apiPost(path, body) {
54
- const headers = getAuthHeaders();
54
+ const headers = await getAuthHeaders();
55
55
  if (!headers)
56
56
  throw new Error([
57
57
  '❌ App Store Connect 인증이 필요해.',
@@ -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`, {
@@ -421,7 +475,7 @@ export async function submitVersionForReview(versionId) {
421
475
  // 자동 제출(submission)은 권장 가이드라인 (스크린샷·리뷰 노트 등)이 충족돼야 통과.
422
476
  const IAP_V2_BASE = 'https://api.appstoreconnect.apple.com/v2';
423
477
  async function apiPostV2(path, body) {
424
- const headers = getAuthHeaders();
478
+ const headers = await getAuthHeaders();
425
479
  if (!headers) {
426
480
  throw new Error('App Store Connect 인증 필요 — npx -p @yoonion/mimi-seed-mcp mimi-seed-appstore-auth');
427
481
  }
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.12",
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": {
@@ -47,12 +47,11 @@
47
47
  "@modelcontextprotocol/sdk": "^1.12.1",
48
48
  "@onesub/providers": "^0.2.0",
49
49
  "googleapis": "^171.4.0",
50
- "jsonwebtoken": "^9.0.3",
50
+ "jose": "^5.10.0",
51
51
  "open": "^10.1.0",
52
52
  "zod": "^3.24.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@types/jsonwebtoken": "^9.0.10",
56
55
  "@types/node": "^22.0.0",
57
56
  "tsx": "^4.19.0",
58
57
  "typescript": "^5.7.0"