@yoonion/mimi-seed-mcp 0.13.15 → 0.13.17

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 CHANGED
@@ -78,8 +78,8 @@ export ANTHROPIC_API_KEY=sk-ant-...
78
78
 
79
79
  | 영역 | 도구 수 | 주요 도구 |
80
80
  |------|---------|-----------|
81
- | App Store Connect | 41 | `appstore_submit_for_review` / `appstore_upload_screenshot` / `appstore_update_product_review_note` / `appstore_upload_product_review_screenshot` |
82
- | Google Play | 32 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
81
+ | App Store Connect | 58 | `appstore_submit_for_review` / `appstore_upload_screenshot` / `appstore_update_product_review_note` / `appstore_upload_product_review_screenshot` |
82
+ | Google Play | 33 | `playstore_submit_release` / `playstore_promote_release` / `playstore_replace_images` / `playstore_reply_review` / `playstore_verify_service_account` |
83
83
  | Firebase | 20 | `firebase_create_project` / `firebase_create_android_app` / `firebase_get_android_config` / `firebase_create_ios_app` |
84
84
  | AdMob | 7 | `admob_list_apps` / `admob_create_ad_unit` / `admob_get_today_earnings` / `admob_get_report` |
85
85
  | CI/CD (GitHub Actions · GitLab) | 6 | `ci_trigger_build` / `ci_get_build_status` / `ci_list_workflows` / `ci_cancel_build` |
@@ -54,6 +54,9 @@ you can paste. Pick the row for the job; batching two rows in one `select:` call
54
54
  | Play Store reviews + stats | `select:playstore_list_reviews,playstore_reply_review,playstore_get_statistics,generate_review_reply` |
55
55
  | Play Store IAP / subscriptions | `select:playstore_list_products,playstore_list_inapp_products,playstore_list_subscriptions,playstore_create_onetime_product,playstore_create_subscription,playstore_update_product,playstore_update_product_listing,playstore_update_subscription_listing,playstore_update_product_state,playstore_delete_product` |
56
56
  | App Store / TestFlight | `select:appstore_list_apps,appstore_verify_credentials,appstore_get_app,appstore_list_versions,appstore_create_version,appstore_get_metadata,appstore_update_whats_new,appstore_list_builds,appstore_attach_build,appstore_attach_latest_build,appstore_list_beta_groups,appstore_submit_for_review,appstore_check_submission_risks,appstore_plan_release` |
57
+ | App Store release control (after approval) | `select:appstore_release_status,appstore_release_version,appstore_update_release_type,appstore_phased_release,appstore_list_versions` |
58
+ | Pre-submission declarations (both stores) | `select:appstore_get_age_rating,appstore_update_age_rating,appstore_declare_encryption,appstore_get_availability,appstore_set_territory_availability,playstore_upload_data_safety` |
59
+ | TestFlight external testing | `select:appstore_beta_status,appstore_update_beta_review_detail,appstore_update_beta_test_info,appstore_update_whats_to_test,appstore_submit_beta_review,appstore_set_beta_group_build,appstore_add_beta_testers,appstore_notify_beta_testers,appstore_list_beta_groups,appstore_list_builds` |
57
60
  | App Store review submission (the bundle) | `select:appstore_list_review_submissions,appstore_add_version_to_review_submission,appstore_remove_review_submission_item,appstore_update_version_string,appstore_cancel_review` |
58
61
  | App Store screenshots | `select:appstore_list_app_info_localizations,appstore_get_metadata,appstore_list_screenshots,appstore_upload_screenshot,appstore_delete_screenshot,appstore_delete_screenshot_set,screenshot_validate` |
59
62
  | App Store app info + review notes | `select:appstore_get_app_info,appstore_update_app_info_localization,appstore_create_app_info_localization,appstore_update_localization,appstore_get_review_notes,appstore_update_review_notes` |
@@ -249,9 +252,9 @@ General rules:
249
252
  - **Draft-app track constraint (Play).** Until an app has its first non-internal
250
253
  publish, only the **`internal`** track can be `completed`. `alpha`/`beta`/`production`
251
254
  reject anything but `draft` → error: *"Only releases with status draft may be created
252
- on draft app."* Closed/open testing also needs the **App Content** declarations
253
- (content rating, data safety, target audience) which are **Console-only** the API
254
- cannot set them.
255
+ on draft app."* Closed/open testing also needs the **App Content** declarations. **Data safety** is
256
+ uploadable as a CSV (`playstore_upload_data_safety`); **content rating and target
257
+ audience remain Console-only** — no API sets them.
255
258
  - **A `403` on one write but not another is usually NOT a permissions gap.** Every
256
259
  `playstore_*` write resolves the same credential (`requirePlayStoreAuth`), so if
257
260
  `playstore_upload_image` succeeds but `playstore_update_listing` returns `403`, the
@@ -0,0 +1,73 @@
1
+ /** Apple 이 쓰는 빈도 척도. 필드마다 같은 enum 을 쓴다. */
2
+ export type Frequency = 'NONE' | 'INFREQUENT_OR_MILD' | 'FREQUENT_OR_INTENSE' | 'INFREQUENT' | 'FREQUENT';
3
+ export interface AgeRatingDeclaration {
4
+ [key: string]: string | boolean | undefined;
5
+ }
6
+ export declare function getAgeRating(appId: string): Promise<{
7
+ appInfoId: string;
8
+ declarationId?: string;
9
+ declaration: AgeRatingDeclaration;
10
+ }>;
11
+ /**
12
+ * 연령 등급 설문 갱신 (PATCH /v1/ageRatingDeclarations/{id}).
13
+ * 넘긴 필드만 바뀐다 — Apple 이 부분 갱신을 허용하므로 전체를 다시 보낼 필요가 없다.
14
+ */
15
+ export declare function updateAgeRating(args: {
16
+ appId: string;
17
+ declaration: AgeRatingDeclaration;
18
+ }): Promise<{
19
+ declarationId: string;
20
+ declaration: AgeRatingDeclaration;
21
+ }>;
22
+ /**
23
+ * 수출 규정 선언 생성 (POST /v1/appEncryptionDeclarations).
24
+ * buildIds 를 주면 그 빌드들에 바로 연결한다 (POST …/relationships/builds).
25
+ *
26
+ * Info.plist 의 ITSAppUsesNonExemptEncryption 으로 해결되는 경우가 더 많다 —
27
+ * 이 도구는 그게 없어서 ASC 가 선언을 요구할 때의 경로다.
28
+ */
29
+ export declare function declareEncryption(args: {
30
+ appId: string;
31
+ appDescription: string;
32
+ containsProprietaryCryptography: boolean;
33
+ containsThirdPartyCryptography: boolean;
34
+ availableOnFrenchStore: boolean;
35
+ buildIds?: string[];
36
+ }): Promise<{
37
+ declarationId: string;
38
+ state?: string;
39
+ attachedBuilds: number;
40
+ }>;
41
+ export interface TerritoryRow {
42
+ /** territoryAvailability 리소스 id — PATCH 할 때 그대로 쓴다. */
43
+ id: string;
44
+ available?: boolean;
45
+ releaseDate?: string;
46
+ preOrderEnabled?: boolean;
47
+ }
48
+ /**
49
+ * 현재 판매 지역 상태 (GET /v1/apps/{id}/appAvailabilityV2 → territoryAvailabilities).
50
+ * 지역이 175개라 기본은 판매 중인 곳만 세고, `includeTerritories` 로 목록을 받는다.
51
+ */
52
+ export declare function getAvailability(args: {
53
+ appId: string;
54
+ includeTerritories?: boolean;
55
+ limit?: number;
56
+ }): Promise<{
57
+ availabilityId?: string;
58
+ availableInNewTerritories?: boolean;
59
+ availableCount: number;
60
+ unavailableCount: number;
61
+ territories?: TerritoryRow[];
62
+ }>;
63
+ /**
64
+ * 지역별 판매 여부·출시일 변경 (PATCH /v1/territoryAvailabilities/{id}).
65
+ * id 는 appstore_get_availability 결과에서 온다 — 추측해서 만들지 말 것.
66
+ */
67
+ export declare function setTerritoryAvailability(args: {
68
+ territories: TerritoryRow[];
69
+ }): Promise<Array<{
70
+ id: string;
71
+ ok: boolean;
72
+ error?: string;
73
+ }>>;
@@ -0,0 +1,163 @@
1
+ // 심사 제출 **전에** 채워야 하는 선언들 — 비어 있으면 제출 자체가 막히거나 심사에서 반려된다.
2
+ //
3
+ // 연령 등급 (ageRatingDeclarations) : appInfo 에 딸린 단일 리소스. PATCH 로만 바꾼다
4
+ // 수출 규정 (appEncryptionDeclarations) : 앱 단위로 만들고 빌드에 붙인다
5
+ // 판매 지역 (appAvailabilityV2 / territoryAvailabilities): 지역별 available·출시일
6
+ //
7
+ // Play 쪽 대응물(데이터 안전 CSV)은 playstore/tools.ts 에 있다 — 자격증명 계통이 달라서 파일을 나눴다.
8
+ import { V1_BASE, V2_BASE, apiRequest, authHeadersOrThrow, isNotFound } from './http.js';
9
+ async function get(base, path, params) {
10
+ const headers = await authHeadersOrThrow();
11
+ const query = params ? `?${new URLSearchParams(params).toString()}` : '';
12
+ return apiRequest(base, `${path}${query}`, headers, { method: 'GET' });
13
+ }
14
+ async function send(base, method, path, body) {
15
+ const headers = await authHeadersOrThrow();
16
+ return apiRequest(base, path, headers, {
17
+ method,
18
+ headers: { 'Content-Type': 'application/json' },
19
+ body: JSON.stringify(body),
20
+ });
21
+ }
22
+ /** 연령 등급은 appInfo 에 딸려 있다. 편집 가능한 appInfo 를 우선 고른다. */
23
+ async function resolveAppInfoId(appId) {
24
+ const data = await get(V1_BASE, `/apps/${appId}/appInfos`, { 'fields[appInfos]': 'state' });
25
+ const infos = data.data ?? [];
26
+ if (infos.length === 0)
27
+ throw new Error(`앱 ${appId} 의 appInfo 를 찾지 못했다.`);
28
+ const editable = infos.find((i) => {
29
+ const s = i.attributes?.state ?? i.attributes?.appStoreState;
30
+ return s && s !== 'READY_FOR_DISTRIBUTION' && s !== 'REPLACED_WITH_NEW_INFO';
31
+ });
32
+ return (editable ?? infos[0]).id;
33
+ }
34
+ export async function getAgeRating(appId) {
35
+ const appInfoId = await resolveAppInfoId(appId);
36
+ // 선언 리소스가 아직 없는 앱이 있다 — 그때는 404 다. 에러 대신 "없음"으로 돌려주고
37
+ // updateAgeRating 이 사람이 읽을 안내를 내도록 한다.
38
+ try {
39
+ const data = await get(V1_BASE, `/appInfos/${appInfoId}/ageRatingDeclaration`);
40
+ return {
41
+ appInfoId,
42
+ declarationId: data.data?.id,
43
+ declaration: data.data?.attributes ?? {},
44
+ };
45
+ }
46
+ catch (err) {
47
+ if (isNotFound(err))
48
+ return { appInfoId, declarationId: undefined, declaration: {} };
49
+ throw err;
50
+ }
51
+ }
52
+ /**
53
+ * 연령 등급 설문 갱신 (PATCH /v1/ageRatingDeclarations/{id}).
54
+ * 넘긴 필드만 바뀐다 — Apple 이 부분 갱신을 허용하므로 전체를 다시 보낼 필요가 없다.
55
+ */
56
+ export async function updateAgeRating(args) {
57
+ const { appId, declaration } = args;
58
+ const attributes = Object.fromEntries(Object.entries(declaration).filter(([, v]) => v !== undefined));
59
+ if (Object.keys(attributes).length === 0) {
60
+ throw new Error('바꿀 항목이 하나도 없다 — declaration 에 최소 한 필드는 넣어야 한다.');
61
+ }
62
+ const current = await getAgeRating(appId);
63
+ if (!current.declarationId) {
64
+ throw new Error([
65
+ '❌ 이 앱에는 연령 등급 선언 리소스가 없다.',
66
+ 'App Store Connect 에서 앱을 한 번 연 뒤 다시 시도하거나, appInfo 상태를 확인할 것.',
67
+ ].join('\n'));
68
+ }
69
+ await send(V1_BASE, 'PATCH', `/ageRatingDeclarations/${current.declarationId}`, {
70
+ data: { type: 'ageRatingDeclarations', id: current.declarationId, attributes },
71
+ });
72
+ const after = await getAgeRating(appId);
73
+ return { declarationId: current.declarationId, declaration: after.declaration };
74
+ }
75
+ /**
76
+ * 수출 규정 선언 생성 (POST /v1/appEncryptionDeclarations).
77
+ * buildIds 를 주면 그 빌드들에 바로 연결한다 (POST …/relationships/builds).
78
+ *
79
+ * Info.plist 의 ITSAppUsesNonExemptEncryption 으로 해결되는 경우가 더 많다 —
80
+ * 이 도구는 그게 없어서 ASC 가 선언을 요구할 때의 경로다.
81
+ */
82
+ export async function declareEncryption(args) {
83
+ const { appId, buildIds = [], ...attributes } = args;
84
+ const created = await send(V1_BASE, 'POST', '/appEncryptionDeclarations', {
85
+ data: {
86
+ type: 'appEncryptionDeclarations',
87
+ attributes,
88
+ relationships: { app: { data: { type: 'apps', id: appId } } },
89
+ },
90
+ });
91
+ const declarationId = created.data?.id;
92
+ if (!declarationId)
93
+ throw new Error(`수출 규정 선언 생성 응답에 id 가 없다: ${JSON.stringify(created)}`);
94
+ if (buildIds.length > 0) {
95
+ await send(V1_BASE, 'POST', `/appEncryptionDeclarations/${declarationId}/relationships/builds`, {
96
+ data: buildIds.map((id) => ({ type: 'builds', id })),
97
+ });
98
+ }
99
+ return {
100
+ declarationId,
101
+ state: created.data?.attributes?.state,
102
+ attachedBuilds: buildIds.length,
103
+ };
104
+ }
105
+ /**
106
+ * 현재 판매 지역 상태 (GET /v1/apps/{id}/appAvailabilityV2 → territoryAvailabilities).
107
+ * 지역이 175개라 기본은 판매 중인 곳만 세고, `includeTerritories` 로 목록을 받는다.
108
+ */
109
+ export async function getAvailability(args) {
110
+ const { appId, includeTerritories = false, limit = 200 } = args;
111
+ const av = await get(V1_BASE, `/apps/${appId}/appAvailabilityV2`);
112
+ const availabilityId = av.data?.id;
113
+ if (!availabilityId) {
114
+ return { availableCount: 0, unavailableCount: 0, availableInNewTerritories: undefined };
115
+ }
116
+ const rows = await get(V2_BASE, `/appAvailabilities/${availabilityId}/territoryAvailabilities`, {
117
+ limit: String(Math.min(limit, 200)),
118
+ });
119
+ const list = (rows.data ?? []).map((t) => ({
120
+ id: t.id,
121
+ available: t.attributes?.available,
122
+ releaseDate: t.attributes?.releaseDate,
123
+ preOrderEnabled: t.attributes?.preOrderEnabled,
124
+ }));
125
+ return {
126
+ availabilityId,
127
+ availableInNewTerritories: av.data?.attributes?.availableInNewTerritories,
128
+ availableCount: list.filter((t) => t.available).length,
129
+ unavailableCount: list.filter((t) => t.available === false).length,
130
+ ...(includeTerritories ? { territories: list } : {}),
131
+ };
132
+ }
133
+ /**
134
+ * 지역별 판매 여부·출시일 변경 (PATCH /v1/territoryAvailabilities/{id}).
135
+ * id 는 appstore_get_availability 결과에서 온다 — 추측해서 만들지 말 것.
136
+ */
137
+ export async function setTerritoryAvailability(args) {
138
+ const results = [];
139
+ for (const t of args.territories) {
140
+ const attributes = {};
141
+ if (t.available !== undefined)
142
+ attributes.available = t.available;
143
+ if (t.releaseDate !== undefined)
144
+ attributes.releaseDate = t.releaseDate;
145
+ if (t.preOrderEnabled !== undefined)
146
+ attributes.preOrderEnabled = t.preOrderEnabled;
147
+ if (Object.keys(attributes).length === 0) {
148
+ results.push({ id: t.id, ok: false, error: '바꿀 필드가 없음' });
149
+ continue;
150
+ }
151
+ try {
152
+ await send(V1_BASE, 'PATCH', `/territoryAvailabilities/${t.id}`, {
153
+ data: { type: 'territoryAvailabilities', id: t.id, attributes },
154
+ });
155
+ results.push({ id: t.id, ok: true });
156
+ }
157
+ catch (err) {
158
+ // 한 지역 실패가 나머지를 막지 않게 한다 — 어디까지 됐는지 그대로 보고한다.
159
+ results.push({ id: t.id, ok: false, error: err.message });
160
+ }
161
+ }
162
+ return results;
163
+ }
@@ -3,3 +3,11 @@ export declare const V2_BASE = "https://api.appstoreconnect.apple.com/v2";
3
3
  export type AppStoreProductType = 'subscription' | 'consumable' | 'non_consumable';
4
4
  export declare function authHeadersOrThrow(): Promise<Record<string, string>>;
5
5
  export declare function apiRequest<T>(base: string, resourcePath: string, authHeaders: Record<string, string>, init: RequestInit): Promise<T>;
6
+ /**
7
+ * "리소스 없음"인지 **상태 코드로** 판별한다.
8
+ *
9
+ * friendlyAppStoreError 가 cause.status 에 실제 HTTP 상태를 붙여준다. 메시지 문자열로
10
+ * 404 를 찾으면 본문에 'not found'/'404' 가 섞인 403·409 까지 "없음"으로 삼켜서,
11
+ * 권한 오류가 조용히 빈 결과로 둔갑한다. 상태를 못 읽는 경우에만 문자열로 폴백한다.
12
+ */
13
+ export declare function isNotFound(err: unknown): boolean;
@@ -26,3 +26,16 @@ export async function apiRequest(base, resourcePath, authHeaders, init) {
26
26
  const text = await response.text();
27
27
  return (text ? JSON.parse(text) : { ok: true });
28
28
  }
29
+ /**
30
+ * "리소스 없음"인지 **상태 코드로** 판별한다.
31
+ *
32
+ * friendlyAppStoreError 가 cause.status 에 실제 HTTP 상태를 붙여준다. 메시지 문자열로
33
+ * 404 를 찾으면 본문에 'not found'/'404' 가 섞인 403·409 까지 "없음"으로 삼켜서,
34
+ * 권한 오류가 조용히 빈 결과로 둔갑한다. 상태를 못 읽는 경우에만 문자열로 폴백한다.
35
+ */
36
+ export function isNotFound(err) {
37
+ const cause = err?.cause;
38
+ if (typeof cause?.status === 'number')
39
+ return cause.status === 404;
40
+ return /App Store API 404\b/.test(err?.message ?? '');
41
+ }
@@ -0,0 +1,58 @@
1
+ export type AppleReleaseType = 'MANUAL' | 'AFTER_APPROVAL' | 'SCHEDULED';
2
+ export type PhasedReleaseState = 'INACTIVE' | 'ACTIVE' | 'PAUSED' | 'COMPLETE';
3
+ export type PhasedReleaseAction = 'status' | 'enable' | 'pause' | 'resume' | 'complete' | 'disable';
4
+ export interface VersionReleaseSummary {
5
+ versionId: string;
6
+ versionString?: string;
7
+ state?: string;
8
+ releaseType?: string;
9
+ earliestReleaseDate?: string;
10
+ }
11
+ export interface PhasedReleaseSummary {
12
+ id: string;
13
+ state?: PhasedReleaseState;
14
+ /** 1~7. Apple 의 7일 램프에서 현재 며칠째인지. */
15
+ currentDayNumber?: number;
16
+ startDate?: string;
17
+ totalPauseDuration?: number;
18
+ }
19
+ export declare function getVersionSummary(versionId: string): Promise<VersionReleaseSummary>;
20
+ export declare function stateNote(state?: string): string;
21
+ /** 버전 상태 + 단계적 출시 상태를 한 번에. 쓰기 전 미리보기용. */
22
+ export declare function getReleaseStatus(versionId: string): Promise<{
23
+ version: VersionReleaseSummary;
24
+ phased: PhasedReleaseSummary | null;
25
+ note: string;
26
+ }>;
27
+ /**
28
+ * 개발자 출시 대기 중인 버전을 지금 출시한다 (POST /v1/appStoreVersionReleaseRequests).
29
+ * 콘솔의 "이 버전 출시" 버튼과 같은 동작 — 되돌릴 수 없다.
30
+ */
31
+ export declare function requestRelease(versionId: string): Promise<VersionReleaseSummary>;
32
+ /**
33
+ * 이미 만들어진 버전의 출시 방식을 바꾼다 (PATCH /v1/appStoreVersions/{id}).
34
+ * MANUAL 로 만들어 두고 "역시 승인되면 바로 내보내자"로 바꾸는 경우가 대부분.
35
+ * SCHEDULED 는 earliestReleaseDate(ISO 8601, 미래)가 함께 필요하다.
36
+ */
37
+ export declare function updateReleaseType(args: {
38
+ versionId: string;
39
+ releaseType: AppleReleaseType;
40
+ earliestReleaseDate?: string;
41
+ }): Promise<VersionReleaseSummary>;
42
+ /**
43
+ * 단계적 출시(7일 램프) 제어.
44
+ *
45
+ * - enable : 없으면 만들고(ACTIVE), PAUSED 면 재개한다
46
+ * - pause : 일시중지 (되돌릴 수 있음)
47
+ * - resume : 재개
48
+ * - complete : 남은 사용자에게 즉시 전체 공개 — 되돌릴 수 없다
49
+ * - disable : 단계적 출시 자체를 제거. 출시 전이면 일반(전체) 출시로, 출시 후면 즉시 전체 공개가 된다
50
+ */
51
+ export declare function setPhasedRelease(args: {
52
+ versionId: string;
53
+ action: Exclude<PhasedReleaseAction, 'status'>;
54
+ }): Promise<{
55
+ action: string;
56
+ phased: PhasedReleaseSummary | null;
57
+ version: VersionReleaseSummary;
58
+ }>;
@@ -0,0 +1,173 @@
1
+ // App Store 출시 제어 — 심사 통과 **이후**의 단계.
2
+ //
3
+ // 버전 생성 때 releaseType 을 정하는 것(appstore_create_version)까지는 이미 되지만,
4
+ // 그 뒤 세 가지가 API 로 안 됐다:
5
+ // 1. 이미 PENDING_DEVELOPER_RELEASE 로 대기 중인 버전을 "지금 출시" (releaseRequests)
6
+ // 2. 만들어 둔 버전의 releaseType 을 나중에 바꾸기 (PATCH appStoreVersions)
7
+ // 3. 단계적 출시 시작·일시중지·재개·즉시완료 (phasedRelease)
8
+ // Play 는 userFraction/halted 로 3번이 되는데 iOS 만 비어 있었다.
9
+ import { V1_BASE, apiRequest, authHeadersOrThrow, isNotFound } from './http.js';
10
+ /** 각 상태가 "지금 출시" 요청을 받을 수 있는지 + 사람이 읽을 설명. */
11
+ const STATE_NOTE = {
12
+ PENDING_DEVELOPER_RELEASE: '심사 통과 후 개발자 출시 대기 — 지금 출시할 수 있다.',
13
+ PENDING_APPLE_RELEASE: '예약 출시 대기 — Apple 이 earliestReleaseDate 에 출시한다.',
14
+ READY_FOR_SALE: '이미 출시됨.',
15
+ WAITING_FOR_REVIEW: '심사 대기열에 있음.',
16
+ IN_REVIEW: '심사 진행 중.',
17
+ PREPARE_FOR_SUBMISSION: '아직 제출 전 — appstore_submit_for_review 먼저.',
18
+ DEVELOPER_REJECTED: '개발자가 회수함 — 다시 제출해야 한다.',
19
+ REJECTED: 'Apple 이 거절함 — 수정 후 재제출.',
20
+ METADATA_REJECTED: '메타데이터 거절 — 수정 후 재제출.',
21
+ };
22
+ async function get(path, params) {
23
+ const headers = await authHeadersOrThrow();
24
+ const query = params ? `?${new URLSearchParams(params).toString()}` : '';
25
+ return apiRequest(V1_BASE, `${path}${query}`, headers, { method: 'GET' });
26
+ }
27
+ async function send(method, path, body) {
28
+ const headers = await authHeadersOrThrow();
29
+ return apiRequest(V1_BASE, path, headers, {
30
+ method,
31
+ ...(body === undefined
32
+ ? {}
33
+ : { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }),
34
+ });
35
+ }
36
+ export async function getVersionSummary(versionId) {
37
+ const data = await get(`/appStoreVersions/${versionId}`, { 'fields[appStoreVersions]': 'versionString,appStoreState,releaseType,earliestReleaseDate' });
38
+ const a = data.data?.attributes ?? {};
39
+ return {
40
+ versionId,
41
+ versionString: a.versionString,
42
+ state: a.appStoreState,
43
+ releaseType: a.releaseType,
44
+ earliestReleaseDate: a.earliestReleaseDate,
45
+ };
46
+ }
47
+ /** to-one 관계는 없을 때 200 + data:null 로 오기도 하고 404 로 오기도 한다. 둘 다 "없음"으로 본다. */
48
+ async function getPhasedRelease(versionId) {
49
+ try {
50
+ const data = await get(`/appStoreVersions/${versionId}/appStoreVersionPhasedRelease`);
51
+ if (!data.data)
52
+ return null;
53
+ const a = (data.data.attributes ?? {});
54
+ return {
55
+ id: data.data.id,
56
+ state: a.phasedReleaseState,
57
+ currentDayNumber: a.currentDayNumber,
58
+ startDate: a.startDate,
59
+ totalPauseDuration: a.totalPauseDuration,
60
+ };
61
+ }
62
+ catch (err) {
63
+ if (isNotFound(err))
64
+ return null;
65
+ throw err;
66
+ }
67
+ }
68
+ export function stateNote(state) {
69
+ return (state && STATE_NOTE[state]) || '';
70
+ }
71
+ /** 버전 상태 + 단계적 출시 상태를 한 번에. 쓰기 전 미리보기용. */
72
+ export async function getReleaseStatus(versionId) {
73
+ const version = await getVersionSummary(versionId);
74
+ const phased = await getPhasedRelease(versionId);
75
+ return { version, phased, note: stateNote(version.state) };
76
+ }
77
+ /**
78
+ * 개발자 출시 대기 중인 버전을 지금 출시한다 (POST /v1/appStoreVersionReleaseRequests).
79
+ * 콘솔의 "이 버전 출시" 버튼과 같은 동작 — 되돌릴 수 없다.
80
+ */
81
+ export async function requestRelease(versionId) {
82
+ const version = await getVersionSummary(versionId);
83
+ if (version.state !== 'PENDING_DEVELOPER_RELEASE') {
84
+ throw new Error([
85
+ `❌ 지금 출시할 수 없는 상태다: ${version.state ?? '(알 수 없음)'}`,
86
+ stateNote(version.state),
87
+ '',
88
+ '출시 요청은 심사를 통과해 PENDING_DEVELOPER_RELEASE 로 대기 중인 버전에만 보낼 수 있다.',
89
+ ]
90
+ .filter(Boolean)
91
+ .join('\n'));
92
+ }
93
+ await send('POST', '/appStoreVersionReleaseRequests', {
94
+ data: {
95
+ type: 'appStoreVersionReleaseRequests',
96
+ relationships: { appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } } },
97
+ },
98
+ });
99
+ return getVersionSummary(versionId);
100
+ }
101
+ /**
102
+ * 이미 만들어진 버전의 출시 방식을 바꾼다 (PATCH /v1/appStoreVersions/{id}).
103
+ * MANUAL 로 만들어 두고 "역시 승인되면 바로 내보내자"로 바꾸는 경우가 대부분.
104
+ * SCHEDULED 는 earliestReleaseDate(ISO 8601, 미래)가 함께 필요하다.
105
+ */
106
+ export async function updateReleaseType(args) {
107
+ const { versionId, releaseType, earliestReleaseDate } = args;
108
+ if (releaseType === 'SCHEDULED' && !earliestReleaseDate) {
109
+ throw new Error('releaseType=SCHEDULED 에는 earliestReleaseDate(ISO 8601, 미래 시각)가 필요하다.');
110
+ }
111
+ const attributes = { releaseType };
112
+ // SCHEDULED 가 아닌데 날짜가 남아 있으면 Apple 이 400 을 낸다 — 명시적으로 비운다.
113
+ if (releaseType === 'SCHEDULED')
114
+ attributes.earliestReleaseDate = earliestReleaseDate;
115
+ else if (earliestReleaseDate === undefined)
116
+ attributes.earliestReleaseDate = null;
117
+ await send('PATCH', `/appStoreVersions/${versionId}`, {
118
+ data: { type: 'appStoreVersions', id: versionId, attributes },
119
+ });
120
+ return getVersionSummary(versionId);
121
+ }
122
+ /**
123
+ * 단계적 출시(7일 램프) 제어.
124
+ *
125
+ * - enable : 없으면 만들고(ACTIVE), PAUSED 면 재개한다
126
+ * - pause : 일시중지 (되돌릴 수 있음)
127
+ * - resume : 재개
128
+ * - complete : 남은 사용자에게 즉시 전체 공개 — 되돌릴 수 없다
129
+ * - disable : 단계적 출시 자체를 제거. 출시 전이면 일반(전체) 출시로, 출시 후면 즉시 전체 공개가 된다
130
+ */
131
+ export async function setPhasedRelease(args) {
132
+ const { versionId, action } = args;
133
+ const version = await getVersionSummary(versionId);
134
+ const current = await getPhasedRelease(versionId);
135
+ if (action === 'enable') {
136
+ if (!current) {
137
+ const created = await send('POST', '/appStoreVersionPhasedReleases', {
138
+ data: {
139
+ type: 'appStoreVersionPhasedReleases',
140
+ attributes: { phasedReleaseState: 'ACTIVE' },
141
+ relationships: { appStoreVersion: { data: { type: 'appStoreVersions', id: versionId } } },
142
+ },
143
+ });
144
+ void created;
145
+ }
146
+ else if (current.state === 'PAUSED') {
147
+ await patchState(current.id, 'ACTIVE');
148
+ }
149
+ return { action, phased: await getPhasedRelease(versionId), version };
150
+ }
151
+ if (!current) {
152
+ throw new Error([
153
+ `❌ 이 버전에는 단계적 출시가 설정돼 있지 않다 (버전 상태: ${version.state ?? '알 수 없음'}).`,
154
+ '먼저 action="enable" 로 켜야 한다.',
155
+ ].join('\n'));
156
+ }
157
+ if (action === 'disable') {
158
+ await send('DELETE', `/appStoreVersionPhasedReleases/${current.id}`);
159
+ return { action, phased: null, version };
160
+ }
161
+ const next = action === 'pause' ? 'PAUSED' : action === 'resume' ? 'ACTIVE' : 'COMPLETE';
162
+ await patchState(current.id, next);
163
+ return { action, phased: await getPhasedRelease(versionId), version };
164
+ }
165
+ async function patchState(phasedReleaseId, state) {
166
+ await send('PATCH', `/appStoreVersionPhasedReleases/${phasedReleaseId}`, {
167
+ data: {
168
+ type: 'appStoreVersionPhasedReleases',
169
+ id: phasedReleaseId,
170
+ attributes: { phasedReleaseState: state },
171
+ },
172
+ });
173
+ }
@@ -0,0 +1,89 @@
1
+ export interface BetaStatus {
2
+ buildId: string;
3
+ internalState?: string;
4
+ externalState?: string;
5
+ note: string;
6
+ autoNotifyEnabled?: boolean;
7
+ submissionState?: string;
8
+ whatsToTestLocales: string[];
9
+ reviewDetail?: {
10
+ id: string;
11
+ complete: boolean;
12
+ missing: string[];
13
+ };
14
+ testInfoLocales?: string[];
15
+ }
16
+ /**
17
+ * 외부 테스트 제출 전 "뭐가 비었는지"를 한 번에 본다.
18
+ * appId 를 함께 주면 앱 단위 항목(심사 정보·테스트 정보)까지 검사한다.
19
+ */
20
+ export declare function getBetaStatus(args: {
21
+ buildId: string;
22
+ appId?: string;
23
+ }): Promise<BetaStatus>;
24
+ /** 베타 심사 정보 (앱 단위, 단일 리소스). PATCH 만 가능하다 — Apple 이 앱 생성 때 만들어 둔다. */
25
+ export declare function updateBetaReviewDetail(args: {
26
+ appId: string;
27
+ fields: Record<string, string | boolean | undefined>;
28
+ }): Promise<{
29
+ id: string;
30
+ attributes: Record<string, unknown>;
31
+ }>;
32
+ /** 앱 단위 테스트 정보(피드백 이메일·설명 등)를 로케일별로 upsert. */
33
+ export declare function upsertBetaTestInfo(args: {
34
+ appId: string;
35
+ locale: string;
36
+ fields: {
37
+ feedbackEmail?: string;
38
+ description?: string;
39
+ marketingUrl?: string;
40
+ privacyPolicyUrl?: string;
41
+ };
42
+ }): Promise<{
43
+ id: string;
44
+ created: boolean;
45
+ locale: string;
46
+ }>;
47
+ /** 빌드 단위 What to Test 를 로케일별로 upsert. */
48
+ export declare function upsertWhatsToTest(args: {
49
+ buildId: string;
50
+ locale: string;
51
+ whatsNew: string;
52
+ }): Promise<{
53
+ id: string;
54
+ created: boolean;
55
+ locale: string;
56
+ }>;
57
+ /** 빌드를 베타 심사에 제출 (외부 테스터 배포 전 필수). */
58
+ export declare function submitBetaReview(buildId: string): Promise<{
59
+ submissionId: string;
60
+ state?: string;
61
+ }>;
62
+ /** 베타 그룹에 빌드를 붙이거나 뗀다. 외부 그룹이면 실제 배포/회수다. */
63
+ export declare function setBetaGroupBuild(args: {
64
+ groupId: string;
65
+ buildId: string;
66
+ action: 'add' | 'remove';
67
+ }): Promise<{
68
+ groupId: string;
69
+ buildId: string;
70
+ action: string;
71
+ }>;
72
+ /** 테스터 초대. 이미 등록된 이메일은 409 가 나므로 개별 결과로 보고한다. */
73
+ export declare function addBetaTesters(args: {
74
+ groupId: string;
75
+ testers: Array<{
76
+ email: string;
77
+ firstName?: string;
78
+ lastName?: string;
79
+ }>;
80
+ }): Promise<Array<{
81
+ email: string;
82
+ ok: boolean;
83
+ testerId?: string;
84
+ error?: string;
85
+ }>>;
86
+ /** 이미 배포된 빌드에 대해 테스터에게 알림을 다시 보낸다. */
87
+ export declare function notifyBetaTesters(buildId: string): Promise<{
88
+ notificationId: string;
89
+ }>;