@yoonion/mimi-seed-mcp 0.13.7 → 0.13.9

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.
@@ -166,6 +166,7 @@ export declare function submitVersionForReview(versionId: string): Promise<{
166
166
  versionId: string;
167
167
  reusedSubmission: boolean;
168
168
  itemAttached: boolean;
169
+ recoveredFromStaleSubmission: boolean;
169
170
  state: any;
170
171
  }>;
171
172
  export declare function addProductToReviewSubmission(args: {
@@ -599,38 +599,73 @@ export async function buildSubmitForReviewPreview(versionId) {
599
599
  whatsNewByLocale,
600
600
  };
601
601
  }
602
+ async function createReviewSubmission(appId, platform) {
603
+ const created = await apiPost('/reviewSubmissions', {
604
+ data: {
605
+ type: 'reviewSubmissions',
606
+ attributes: { platform },
607
+ relationships: {
608
+ app: { data: { type: 'apps', id: appId } },
609
+ },
610
+ },
611
+ });
612
+ const submissionId = created?.data?.id;
613
+ if (!submissionId) {
614
+ throw new Error(`reviewSubmission 생성 응답에 id가 없어: ${JSON.stringify(created)}`);
615
+ }
616
+ return submissionId;
617
+ }
618
+ function isItemAddRejected(error) {
619
+ const cause = error?.cause;
620
+ if (cause?.status !== 409)
621
+ return false;
622
+ return (cause.parsedErrors ?? []).some((e) => (e.code ?? '').startsWith('STATE_ERROR'));
623
+ }
602
624
  export async function submitVersionForReview(versionId) {
603
625
  const { appId, platform } = await getVersionAppAndPlatform(versionId);
604
- // 1. CREATED 상태의 reviewSubmission이 있으면 재사용, 없으면 새로 생성
626
+ // 1. 열린 reviewSubmission이 있으면 재사용, 없으면 새로 생성
605
627
  let submissionId = await findOpenReviewSubmission(appId, platform);
606
- const reusedSubmission = Boolean(submissionId);
628
+ let reusedSubmission = Boolean(submissionId);
629
+ // findOpenReviewSubmission 은 WAITING_FOR_REVIEW 도 잡아온다. 그 상태의 실제 진행도는
630
+ // API 의 state 필드보다 앞서 있을 수 있어(실측: 이미 심사 큐를 탄 옛 제출), 항목 추가
631
+ // 자체를 거부당하는 경우가 있다 — 아래 recoveredFromStaleSubmission 이 그 케이스다.
632
+ let recoveredFromStaleSubmission = false;
607
633
  if (!submissionId) {
608
- const created = await apiPost('/reviewSubmissions', {
609
- data: {
610
- type: 'reviewSubmissions',
611
- attributes: { platform },
612
- relationships: {
613
- app: { data: { type: 'apps', id: appId } },
614
- },
615
- },
616
- });
617
- submissionId = created?.data?.id;
618
- if (!submissionId) {
619
- throw new Error(`reviewSubmission 생성 응답에 id가 없어: ${JSON.stringify(created)}`);
620
- }
634
+ submissionId = await createReviewSubmission(appId, platform);
635
+ reusedSubmission = false;
621
636
  }
622
637
  // 2. 버전을 reviewSubmissionItems로 attach (이미 붙어있으면 skip)
623
- const alreadyAttached = reusedSubmission ? await isVersionAttached(submissionId, versionId) : false;
638
+ let alreadyAttached = reusedSubmission ? await isVersionAttached(submissionId, versionId) : false;
624
639
  if (!alreadyAttached) {
625
- await apiPost('/reviewSubmissionItems', {
626
- data: {
627
- type: 'reviewSubmissionItems',
628
- relationships: {
629
- reviewSubmission: { data: { type: 'reviewSubmissions', id: submissionId } },
630
- appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
640
+ try {
641
+ await apiPost('/reviewSubmissionItems', {
642
+ data: {
643
+ type: 'reviewSubmissionItems',
644
+ relationships: {
645
+ reviewSubmission: { data: { type: 'reviewSubmissions', id: submissionId } },
646
+ appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
647
+ },
631
648
  },
632
- },
633
- });
649
+ });
650
+ }
651
+ catch (error) {
652
+ if (!reusedSubmission || !isItemAddRejected(error))
653
+ throw error;
654
+ // 재사용하려던 묶음이 실제로는 잠겨 있었다 — 새 묶음을 만들어 한 번만 재시도한다.
655
+ submissionId = await createReviewSubmission(appId, platform);
656
+ reusedSubmission = false;
657
+ recoveredFromStaleSubmission = true;
658
+ alreadyAttached = false;
659
+ await apiPost('/reviewSubmissionItems', {
660
+ data: {
661
+ type: 'reviewSubmissionItems',
662
+ relationships: {
663
+ reviewSubmission: { data: { type: 'reviewSubmissions', id: submissionId } },
664
+ appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } },
665
+ },
666
+ },
667
+ });
668
+ }
634
669
  }
635
670
  // 3. PATCH submitted=true → state: CREATED → WAITING_FOR_REVIEW
636
671
  const submitted = await apiPatch(`/reviewSubmissions/${submissionId}`, {
@@ -647,6 +682,7 @@ export async function submitVersionForReview(versionId) {
647
682
  versionId,
648
683
  reusedSubmission,
649
684
  itemAttached: !alreadyAttached,
685
+ recoveredFromStaleSubmission,
650
686
  state: submitted?.data?.attributes?.state ?? 'WAITING_FOR_REVIEW',
651
687
  };
652
688
  }
@@ -0,0 +1,2 @@
1
+ /** Open OAuth in the platform's default private/incognito browser window. */
2
+ export declare function openPrivateBrowser(url: string): Promise<void>;
@@ -0,0 +1,5 @@
1
+ import open, { apps } from 'open';
2
+ /** Open OAuth in the platform's default private/incognito browser window. */
3
+ export async function openPrivateBrowser(url) {
4
+ await open(url, { app: { name: apps.browserPrivate } });
5
+ }
package/dist/auth/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import readline from 'node:readline';
3
- import open from 'open';
4
3
  import { startAuth, getStoredTokens, ensureFreshAccessToken, } from './google-auth.js';
5
4
  import { AuthError, classifyError } from './errors.js';
6
5
  import { getMcpOAuthClient } from './constants.js';
7
6
  import { AUTH_DOMAINS, DOMAIN_IDS, parseDomainList, summarizeGrantedDomains, } from './scopes.js';
7
+ import { openPrivateBrowser } from './browser.js';
8
8
  import { resolveLang } from '../lib/lang.js';
9
9
  // ko 가 원본이고 en 은 `typeof ko` 를 만족해야 한다 — 키를 빠뜨리면 컴파일이 깨진다.
10
10
  // 여기 있는 건 전부 **터미널에 찍히는 사람용 문자열**이다. errors.ts 가 만드는
@@ -60,7 +60,7 @@ const ko = {
60
60
  serverStart: ' 🌐 OAuth 콜백 서버 시작: http://localhost:9876/callback',
61
61
  serverFail: ' ❌ 콜백 서버 시작 실패',
62
62
  pasteUrl: ' 📋 아래 URL을 브라우저에 직접 붙여넣으세요:',
63
- openingBrowser: ' 🌐 기본 브라우저 자동 열기...',
63
+ openingBrowser: ' 🌐 시크릿 브라우저 자동 열기...',
64
64
  openingHint: ' (실패 시 --no-browser 로 URL 직접 받기)',
65
65
  openFail: (msg) => ` ⚠️ 브라우저 자동 열기 실패: ${msg}`,
66
66
  openManually: ' 📋 직접 열어주세요:',
@@ -124,7 +124,7 @@ const en = {
124
124
  serverStart: ' 🌐 Starting the OAuth callback server: http://localhost:9876/callback',
125
125
  serverFail: ' ❌ Failed to start the callback server',
126
126
  pasteUrl: ' 📋 Paste this URL into your browser:',
127
- openingBrowser: ' 🌐 Opening your default browser...',
127
+ openingBrowser: ' 🌐 Opening a private browser window...',
128
128
  openingHint: ' (if that fails, use --no-browser to get the URL)',
129
129
  openFail: (msg) => ` ⚠️ Could not open the browser: ${msg}`,
130
130
  openManually: ' 📋 Please open it yourself:',
@@ -337,7 +337,7 @@ async function cmdLogin() {
337
337
  else {
338
338
  err(M.openingBrowser);
339
339
  try {
340
- await open(url);
340
+ await openPrivateBrowser(url);
341
341
  err(M.openingHint);
342
342
  }
343
343
  catch (e) {
@@ -31,7 +31,7 @@ export declare function getAuthenticatedClient(): ReturnType<typeof createOAuth2
31
31
  /**
32
32
  * OAuth 플로우 시작.
33
33
  * URL과 대기 Promise를 즉시 반환. localhost:9876 콜백 서버는 백그라운드로 실행.
34
- * 호출자가 URL을 사용자에게 전달하거나 `open()`을 직접 호출.
34
+ * 호출자가 URL을 사용자에게 전달하거나 private 브라우저를 직접 연다.
35
35
  * `wait` Promise: 토큰 저장 시 resolve, 타임아웃/에러 시 reject.
36
36
  * 재호출 시 기존 세션 자동 정리.
37
37
  *
@@ -47,7 +47,7 @@ export declare function startAuth(clientId: string, clientSecret: string, option
47
47
  wait: Promise<StoredTokens>;
48
48
  };
49
49
  /**
50
- * Interactive login — opens browser, waits for callback.
50
+ * Interactive login — opens a private browser window, waits for callback.
51
51
  * startAuth() 래퍼 — CLI에서 사용.
52
52
  */
53
53
  export declare function login(clientId: string, clientSecret: string, options?: {
@@ -1,11 +1,11 @@
1
1
  import { google } from 'googleapis';
2
2
  import http from 'node:http';
3
- import open from 'open';
4
3
  import fs from 'node:fs';
5
4
  import path from 'node:path';
6
5
  import os from 'node:os';
7
6
  import { getMcpOAuthClient } from './constants.js';
8
7
  import { AuthError, classifyError } from './errors.js';
8
+ import { openPrivateBrowser } from './browser.js';
9
9
  // 스코프 목록의 SSOT 는 scopes.ts (도메인 → 스코프 매핑). 여기서는 로그인 요청 조립만 한다.
10
10
  import { scopesForDomains, mergeScopeStrings } from './scopes.js';
11
11
  // Primary config dir. Legacy `~/.preseed` is read as a fallback during the
@@ -116,7 +116,7 @@ let activeAuthServer = null;
116
116
  /**
117
117
  * OAuth 플로우 시작.
118
118
  * URL과 대기 Promise를 즉시 반환. localhost:9876 콜백 서버는 백그라운드로 실행.
119
- * 호출자가 URL을 사용자에게 전달하거나 `open()`을 직접 호출.
119
+ * 호출자가 URL을 사용자에게 전달하거나 private 브라우저를 직접 연다.
120
120
  * `wait` Promise: 토큰 저장 시 resolve, 타임아웃/에러 시 reject.
121
121
  * 재호출 시 기존 세션 자동 정리.
122
122
  *
@@ -138,7 +138,10 @@ export function startAuth(clientId, clientSecret, options = {}) {
138
138
  const authUrl = oauth2Client.generateAuthUrl({
139
139
  access_type: 'offline',
140
140
  scope: requestedScopes,
141
- prompt: 'consent',
141
+ // Private windows can still share cookies with an already-running private session.
142
+ // Force Google to show the account chooser so an unrelated signed-in account is
143
+ // never selected implicitly.
144
+ prompt: 'consent select_account',
142
145
  include_granted_scopes: true,
143
146
  });
144
147
  const wait = new Promise((resolve, reject) => {
@@ -268,13 +271,13 @@ export function startAuth(clientId, clientSecret, options = {}) {
268
271
  return { url: authUrl, wait };
269
272
  }
270
273
  /**
271
- * Interactive login — opens browser, waits for callback.
274
+ * Interactive login — opens a private browser window, waits for callback.
272
275
  * startAuth() 래퍼 — CLI에서 사용.
273
276
  */
274
277
  export async function login(clientId, clientSecret, options = {}) {
275
278
  const { url, wait } = startAuth(clientId, clientSecret, options);
276
- console.log('🔐 브라우저에서 Google 로그인 중...');
277
- open(url);
279
+ console.log('🔐 시크릿 브라우저에서 Google 계정 선택 중...');
280
+ await openPrivateBrowser(url);
278
281
  return wait;
279
282
  }
280
283
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yoonion/mimi-seed-mcp",
3
- "version": "0.13.7",
3
+ "version": "0.13.9",
4
4
  "description": "Mimi Seed MCP server — Firebase + AdMob + Google Play + App Store management for Claude Code / Codex / Cursor / any MCP client.",
5
5
  "type": "module",
6
6
  "bin": {