@yoonion/mimi-seed-mcp 0.19.0 → 0.19.2

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.
@@ -0,0 +1,307 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ const BILLING_MODULE = /com\.android\.billingclient:billing(?:-ktx)?/;
4
+ const LITERAL_DEPENDENCY = /com\.android\.billingclient:billing(?:-ktx)?:([0-9]+(?:\.[0-9A-Za-z_-]+){0,3})/g;
5
+ const VARIABLE_DEPENDENCY = /com\.android\.billingclient:billing(?:-ktx)?:\$\{?([A-Za-z_][A-Za-z0-9_.-]*)\}?/g;
6
+ const VERSION_ASSIGNMENT = /(?:^|\s)([A-Za-z_][A-Za-z0-9_.-]*)\s*(?:=|:)\s*["']([0-9]+(?:\.[0-9A-Za-z_-]+){0,3})["']/gm;
7
+ const BILLING_SUPPORT_SCHEDULE = [
8
+ { major: 5, submissionDeadline: '2024-08-31', extensionDeadline: '2024-11-01' },
9
+ { major: 6, submissionDeadline: '2025-08-31', extensionDeadline: '2025-11-01' },
10
+ { major: 7, submissionDeadline: '2026-08-31', extensionDeadline: '2026-11-01' },
11
+ { major: 8, submissionDeadline: '2027-08-31', extensionDeadline: '2027-11-01' },
12
+ { major: 9, submissionDeadline: '2028-08-31', extensionDeadline: '2028-11-01' },
13
+ ];
14
+ const SKIP_DIRS = new Set([
15
+ '.git',
16
+ '.gradle',
17
+ '.idea',
18
+ 'build',
19
+ 'dist',
20
+ 'node_modules',
21
+ 'Pods',
22
+ 'DerivedData',
23
+ ]);
24
+ async function walk(root, maxDepth = 7) {
25
+ const result = [];
26
+ async function visit(dir, depth) {
27
+ if (depth > maxDepth)
28
+ return;
29
+ let entries;
30
+ try {
31
+ entries = await fs.readdir(dir, { withFileTypes: true });
32
+ }
33
+ catch {
34
+ return;
35
+ }
36
+ for (const entry of entries) {
37
+ if (entry.isDirectory()) {
38
+ if (!SKIP_DIRS.has(entry.name))
39
+ await visit(path.join(dir, entry.name), depth + 1);
40
+ }
41
+ else if (entry.isFile()
42
+ && (entry.name === 'build.gradle'
43
+ || entry.name === 'build.gradle.kts'
44
+ || entry.name === 'libs.versions.toml')) {
45
+ result.push(path.join(dir, entry.name));
46
+ }
47
+ }
48
+ }
49
+ await visit(root, 0);
50
+ return result;
51
+ }
52
+ function parseCatalog(text) {
53
+ const versions = new Map();
54
+ const libraries = new Map();
55
+ const bundles = new Map();
56
+ let section = '';
57
+ for (const rawLine of text.split(/\r?\n/)) {
58
+ const line = rawLine.replace(/\s+#.*$/, '').trim();
59
+ if (!line)
60
+ continue;
61
+ const sectionMatch = line.match(/^\[([^\]]+)]$/);
62
+ if (sectionMatch) {
63
+ section = sectionMatch[1];
64
+ continue;
65
+ }
66
+ if (section === 'versions') {
67
+ const match = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*["']([^"']+)["']/);
68
+ if (match)
69
+ versions.set(match[1], match[2]);
70
+ continue;
71
+ }
72
+ if (section === 'bundles') {
73
+ const match = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*\[([^\]]*)]/);
74
+ if (match) {
75
+ bundles.set(match[1], [...match[2].matchAll(/["']([^"']+)["']/g)].map((item) => item[1]));
76
+ }
77
+ continue;
78
+ }
79
+ if (section !== 'libraries')
80
+ continue;
81
+ const shorthand = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*["']([^"']+)["']/);
82
+ if (shorthand) {
83
+ const coordinates = shorthand[2].split(':');
84
+ libraries.set(shorthand[1], {
85
+ module: coordinates.length >= 2 ? `${coordinates[0]}:${coordinates[1]}` : undefined,
86
+ version: coordinates.length >= 3 ? coordinates.slice(2).join(':') : undefined,
87
+ });
88
+ continue;
89
+ }
90
+ const match = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*\{(.+)}\s*$/);
91
+ if (!match)
92
+ continue;
93
+ const body = match[2];
94
+ const explicitModule = body.match(/module\s*=\s*["']([^"']+)["']/)?.[1];
95
+ const group = body.match(/group\s*=\s*["']([^"']+)["']/)?.[1];
96
+ const name = body.match(/name\s*=\s*["']([^"']+)["']/)?.[1];
97
+ const module = explicitModule ?? (group && name ? `${group}:${name}` : undefined);
98
+ const version = body.match(/(?:^|,)\s*version\s*=\s*["']([^"']+)["']/)?.[1];
99
+ const versionRef = body.match(/version\.ref\s*=\s*["']([^"']+)["']/)?.[1];
100
+ libraries.set(match[1], { module, version, versionRef });
101
+ }
102
+ return { versions, libraries, bundles };
103
+ }
104
+ function collectVariables(text) {
105
+ const result = new Map();
106
+ for (const match of text.matchAll(VERSION_ASSIGNMENT))
107
+ result.set(match[1], match[2]);
108
+ return result;
109
+ }
110
+ function normalizeAlias(alias) {
111
+ return alias.replace(/^libs\./, '').replace(/\./g, '-');
112
+ }
113
+ function majorOf(version) {
114
+ const major = Number.parseInt(version.split('.')[0], 10);
115
+ return Number.isFinite(major) ? major : null;
116
+ }
117
+ function policyAt(now) {
118
+ const deadlineEnd = (date) => new Date(`${date}T23:59:59.999Z`);
119
+ const next = BILLING_SUPPORT_SCHEDULE.find((row) => now <= deadlineEnd(row.submissionDeadline));
120
+ const lastExpired = [...BILLING_SUPPORT_SCHEDULE]
121
+ .filter((row) => now > deadlineEnd(row.submissionDeadline))
122
+ .at(-1);
123
+ return {
124
+ minimumSupportedMajor: next?.major ?? null,
125
+ submissionDeadline: lastExpired?.submissionDeadline ?? BILLING_SUPPORT_SCHEDULE[0].submissionDeadline,
126
+ extensionDeadline: lastExpired?.extensionDeadline ?? BILLING_SUPPORT_SCHEDULE[0].extensionDeadline,
127
+ latestKnownMajor: BILLING_SUPPORT_SCHEDULE.at(-1).major,
128
+ scheduleCurrent: Boolean(next),
129
+ knownSchedule: BILLING_SUPPORT_SCHEDULE.map((row) => ({ ...row })),
130
+ sourceUrl: 'https://developer.android.com/google/play/billing/deprecation-faq',
131
+ };
132
+ }
133
+ function scheduleForMajor(major) {
134
+ return BILLING_SUPPORT_SCHEDULE.find((row) => row.major === major);
135
+ }
136
+ function catalogScope(file) {
137
+ const parent = path.dirname(file);
138
+ return path.basename(parent) === 'gradle' ? path.dirname(parent) : parent;
139
+ }
140
+ function isWithin(scope, file) {
141
+ const relative = path.relative(scope, file);
142
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
143
+ }
144
+ export async function checkBillingCompliance(projectPath, now = new Date()) {
145
+ const root = path.resolve(projectPath);
146
+ let rootStat;
147
+ try {
148
+ rootStat = await fs.stat(root);
149
+ }
150
+ catch {
151
+ throw new Error(`Android project path does not exist or is not readable: ${root}`);
152
+ }
153
+ if (!rootStat.isDirectory())
154
+ throw new Error(`Android project path is not a directory: ${root}`);
155
+ const files = await walk(root);
156
+ const texts = new Map();
157
+ for (const file of files)
158
+ texts.set(file, await fs.readFile(file, 'utf8'));
159
+ const catalogs = files
160
+ .filter((file) => path.basename(file) === 'libs.versions.toml')
161
+ .map((file) => ({ file, scope: catalogScope(file), info: parseCatalog(texts.get(file) ?? '') }));
162
+ const variablesByFile = new Map();
163
+ for (const [file, text] of texts) {
164
+ if (path.basename(file) === 'libs.versions.toml')
165
+ continue;
166
+ variablesByFile.set(file, collectVariables(text));
167
+ }
168
+ const catalogFor = (file) => catalogs
169
+ .filter((entry) => isWithin(entry.scope, file))
170
+ .sort((left, right) => right.scope.length - left.scope.length)[0]?.info
171
+ ?? (catalogs.length === 1 ? catalogs[0].info : undefined);
172
+ const variableFor = (file, key) => [...variablesByFile.entries()]
173
+ .filter(([candidate]) => candidate === file || isWithin(path.dirname(candidate), file))
174
+ .sort(([left], [right]) => path.dirname(right).length - path.dirname(left).length)
175
+ .map(([, variables]) => variables.get(key))
176
+ .find((value) => value !== undefined);
177
+ const evidence = [];
178
+ for (const [file, text] of texts) {
179
+ const relative = path.relative(root, file).replace(/\\/g, '/');
180
+ if (path.basename(file) === 'libs.versions.toml') {
181
+ continue;
182
+ }
183
+ const catalog = catalogFor(file);
184
+ for (const match of text.matchAll(LITERAL_DEPENDENCY)) {
185
+ evidence.push({
186
+ file: relative,
187
+ module: match[0].slice(0, match[0].lastIndexOf(':')),
188
+ version: match[1],
189
+ source: 'literal',
190
+ });
191
+ }
192
+ for (const match of text.matchAll(VARIABLE_DEPENDENCY)) {
193
+ const version = variableFor(file, match[1]);
194
+ evidence.push({
195
+ file: relative,
196
+ module: match[0].slice(0, match[0].lastIndexOf(':')),
197
+ version,
198
+ expression: match[1],
199
+ source: version ? 'variable' : 'unresolved',
200
+ });
201
+ }
202
+ for (const bundleMatch of text.matchAll(/\blibs\.bundles\.([A-Za-z0-9_.-]+)/g)) {
203
+ const bundleAlias = normalizeAlias(bundleMatch[1]);
204
+ const libraryAliases = catalog?.bundles.get(bundleAlias) ?? catalog?.bundles.get(bundleMatch[1]) ?? [];
205
+ for (const libraryAlias of libraryAliases) {
206
+ const normalizedLibraryAlias = normalizeAlias(libraryAlias);
207
+ const lib = catalog?.libraries.get(normalizedLibraryAlias) ?? catalog?.libraries.get(libraryAlias);
208
+ if (!lib?.module || !BILLING_MODULE.test(lib.module))
209
+ continue;
210
+ const version = lib.version ?? (lib.versionRef ? catalog?.versions.get(lib.versionRef) : undefined);
211
+ if (!evidence.some((row) => row.file === relative && row.expression === bundleMatch[0] && row.module === lib.module)) {
212
+ evidence.push({
213
+ file: relative,
214
+ module: lib.module,
215
+ version,
216
+ expression: bundleMatch[0],
217
+ source: version ? 'version_catalog' : 'unresolved',
218
+ });
219
+ }
220
+ }
221
+ }
222
+ for (const aliasMatch of text.matchAll(/\blibs\.([A-Za-z0-9_.-]+)/g)) {
223
+ if (aliasMatch[1].startsWith('bundles.'))
224
+ continue;
225
+ const alias = normalizeAlias(aliasMatch[1]);
226
+ const lib = catalog?.libraries.get(alias) ?? catalog?.libraries.get(aliasMatch[1]);
227
+ if (!lib?.module || !BILLING_MODULE.test(lib.module))
228
+ continue;
229
+ const version = lib.version ?? (lib.versionRef ? catalog?.versions.get(lib.versionRef) : undefined);
230
+ if (!evidence.some((row) => row.file === relative && row.expression === aliasMatch[0])) {
231
+ evidence.push({
232
+ file: relative,
233
+ module: lib.module,
234
+ version,
235
+ expression: aliasMatch[0],
236
+ source: version ? 'version_catalog' : 'unresolved',
237
+ });
238
+ }
239
+ }
240
+ if (BILLING_MODULE.test(text) && !evidence.some((row) => row.file === relative)) {
241
+ evidence.push({
242
+ file: relative,
243
+ module: 'com.android.billingclient:billing',
244
+ expression: 'Billing dependency found but version could not be resolved',
245
+ source: 'unresolved',
246
+ });
247
+ }
248
+ }
249
+ const detectedVersions = [...new Set(evidence.flatMap((row) => row.version ? [row.version] : []))].sort();
250
+ const policy = policyAt(now);
251
+ const majors = detectedVersions.map(majorOf).filter((major) => major !== null);
252
+ const unresolved = evidence.some((row) => !row.version);
253
+ let status;
254
+ let summary;
255
+ const actions = [];
256
+ if (evidence.length === 0) {
257
+ status = 'not_used';
258
+ summary = 'Google Play Billing dependency was not found in the scanned Gradle project.';
259
+ }
260
+ else if (!policy.scheduleCurrent || policy.minimumSupportedMajor === null) {
261
+ status = 'unresolved';
262
+ summary = `The official schedule embedded in this release ends at Billing Library ${policy.latestKnownMajor}; current policy must be refreshed from the source.`;
263
+ actions.push('Check the official Billing deprecation table and update Mimi Seed before relying on this result.');
264
+ }
265
+ else if (majors.some((major) => major < policy.minimumSupportedMajor)) {
266
+ status = 'blocker';
267
+ summary = `Billing Library ${detectedVersions.join(', ')} is below the submission minimum major ${policy.minimumSupportedMajor}.`;
268
+ actions.push(`Upgrade to a supported Billing Library before submitting a new app or update.`);
269
+ for (const major of [...new Set(majors.filter((value) => value < policy.minimumSupportedMajor))].sort()) {
270
+ const schedule = scheduleForMajor(major);
271
+ actions.push(schedule
272
+ ? `Billing Library ${major}: standard deadline ${schedule.submissionDeadline}; extension deadline ${schedule.extensionDeadline} only if Google granted it in Play Console.`
273
+ : `Billing Library ${major}: its deadline predates the embedded official table; no active extension should be assumed.`);
274
+ }
275
+ }
276
+ else if (unresolved || majors.length === 0) {
277
+ status = 'unresolved';
278
+ summary = 'A Billing dependency was found, but at least one version expression could not be resolved statically.';
279
+ actions.push('Resolve the reported Gradle variable or version catalog alias and run the check again.');
280
+ }
281
+ else if (majors.some((major) => major === policy.minimumSupportedMajor)) {
282
+ status = 'warning';
283
+ summary = `Billing Library ${detectedVersions.join(', ')} is currently supported but is the next major scheduled for deprecation.`;
284
+ const nextDeadline = scheduleForMajor(policy.minimumSupportedMajor);
285
+ if (nextDeadline)
286
+ actions.push(`Plan an upgrade before ${nextDeadline.submissionDeadline}.`);
287
+ }
288
+ else {
289
+ status = 'pass';
290
+ summary = `Billing Library ${detectedVersions.join(', ')} satisfies the current submission policy.`;
291
+ }
292
+ return {
293
+ projectPath: root,
294
+ checkedAt: now.toISOString(),
295
+ status,
296
+ detectedVersions,
297
+ evidence,
298
+ policy,
299
+ summary,
300
+ actions,
301
+ upgrade: {
302
+ installCommand: 'android skills add play-billing-library-version-upgrade',
303
+ prompt: 'Help me upgrade my Play Billing Library implementation.',
304
+ automaticExecution: false,
305
+ },
306
+ };
307
+ }
@@ -8,5 +8,8 @@ export interface SubmissionRisk {
8
8
  fixUrl?: string;
9
9
  }
10
10
  export declare function checkPlayStoreRisks(auth: OAuth2Client, packageName: string, language?: string): Promise<SubmissionRisk[]>;
11
- export declare function checkAppStoreRisks(appId: string): Promise<SubmissionRisk[]>;
11
+ export declare function checkAppStoreRisks(appId: string, expected?: {
12
+ socialMedia?: boolean;
13
+ socialMediaAgeRestricted?: boolean;
14
+ }): Promise<SubmissionRisk[]>;
12
15
  export declare function formatRisks(risks: SubmissionRisk[], platform: string): string;
@@ -1,5 +1,6 @@
1
1
  import { publisher, withEdit } from '../playstore/tools.js';
2
2
  import { apiGet } from '../appstore/tools.js';
3
+ import { isNotFound } from '../appstore/http.js';
3
4
  const APPSTORE_EDITABLE_STATES = 'PREPARE_FOR_SUBMISSION,DEVELOPER_REJECTED,METADATA_REJECTED,REJECTED';
4
5
  export async function checkPlayStoreRisks(auth, packageName, language = 'ko-KR') {
5
6
  const risks = [];
@@ -72,7 +73,7 @@ async function safeGet(call, risks, code, title) {
72
73
  }
73
74
  }
74
75
  const APP_INFO_LIVE_STATE = 'READY_FOR_DISTRIBUTION';
75
- export async function checkAppStoreRisks(appId) {
76
+ export async function checkAppStoreRisks(appId, expected = {}) {
76
77
  const risks = [];
77
78
  const versions = await safeGet(() => apiGet(`/apps/${appId}/appStoreVersions`, {
78
79
  'filter[appStoreState]': APPSTORE_EDITABLE_STATES,
@@ -140,10 +141,25 @@ export async function checkAppStoreRisks(appId) {
140
141
  const stateOf = (i) => i.attributes?.state ?? i.attributes?.appStoreState ?? '';
141
142
  const editable = infos.find((i) => stateOf(i) !== APP_INFO_LIVE_STATE) ?? infos[0];
142
143
  if (editable) {
143
- const localizations = await safeGet(() => apiGet(`/appInfos/${editable.id}/appInfoLocalizations`, {
144
- 'fields[appInfoLocalizations]': 'locale,privacyPolicyUrl,privacyPolicyText',
145
- 'limit': '200',
146
- }), risks, 'PRIVACY', '개인정보 로컬라이제이션');
144
+ const [localizations, ageRating] = await Promise.all([
145
+ safeGet(() => apiGet(`/appInfos/${editable.id}/appInfoLocalizations`, {
146
+ 'fields[appInfoLocalizations]': 'locale,privacyPolicyUrl,privacyPolicyText',
147
+ 'limit': '200',
148
+ }), risks, 'PRIVACY', '개인정보 로컬라이제이션'),
149
+ safeGet(async () => {
150
+ try {
151
+ return await apiGet(`/appInfos/${editable.id}/ageRatingDeclaration`, {
152
+ 'fields[ageRatingDeclarations]': 'userGeneratedContent,socialMedia,socialMediaAgeRestricted,ageAssurance',
153
+ });
154
+ }
155
+ catch (error) {
156
+ // A 404 means the declaration is genuinely absent, not that the check is unknown.
157
+ if (isNotFound(error))
158
+ return { data: null };
159
+ throw error;
160
+ }
161
+ }, risks, 'AGE_RATING_SOCIAL', '연령등급 소셜 미디어 응답'),
162
+ ]);
147
163
  if (localizations) {
148
164
  const locs = (localizations.data ?? []);
149
165
  const hasPrivacy = locs.some((l) => l.attributes?.privacyPolicyUrl || l.attributes?.privacyPolicyText);
@@ -151,6 +167,63 @@ export async function checkAppStoreRisks(appId) {
151
167
  risks.push({ level: 'blocker', code: 'NO_PRIVACY', title: '개인정보처리방침 URL 없음', detail: 'App Store 가이드라인 5.1.1 — 필수 항목입니다. 모든 로컬라이제이션에서 privacyPolicyUrl 또는 privacyPolicyText 중 하나를 입력하세요.' });
152
168
  }
153
169
  }
170
+ if (ageRating) {
171
+ const declaration = ageRating.data?.attributes ?? {};
172
+ if (declaration.socialMedia === undefined || declaration.socialMediaAgeRestricted === undefined) {
173
+ risks.push({
174
+ level: 'blocker',
175
+ code: 'SOCIAL_MEDIA_ANSWERS_MISSING',
176
+ title: '연령등급 소셜 미디어 질문 미응답',
177
+ detail: '2026년 9월부터 신규 앱·업데이트 제출에 socialMedia와 socialMediaAgeRestricted 응답이 필요합니다.',
178
+ fixUrl: 'https://developer.apple.com/news/?id=tlur8uvi',
179
+ });
180
+ }
181
+ if (declaration.socialMediaAgeRestricted === true && declaration.socialMedia !== true) {
182
+ risks.push({
183
+ level: 'blocker',
184
+ code: 'SOCIAL_MEDIA_AGE_RESTRICTION_MISMATCH',
185
+ title: '13세 미만 제한 응답 불일치',
186
+ detail: 'socialMediaAgeRestricted=true는 socialMedia=true인 앱에서만 의미가 있습니다.',
187
+ });
188
+ }
189
+ if (declaration.socialMediaAgeRestricted === true && declaration.ageAssurance !== true) {
190
+ risks.push({
191
+ level: 'blocker',
192
+ code: 'SOCIAL_MEDIA_AGE_ASSURANCE_MISSING',
193
+ title: '13세 미만 소셜 기능 제한에 연령 확인이 없음',
194
+ detail: 'Apple은 socialMediaAgeRestricted=true인 경우 최소한 Declared Age Range API로 연령 범위를 확인하도록 요구합니다. ageAssurance 응답과 실제 구현을 확인하세요.',
195
+ fixUrl: 'https://developer.apple.com/help/app-store-connect/reference/app-information/age-ratings-values-and-definitions',
196
+ });
197
+ }
198
+ if (expected.socialMedia !== undefined
199
+ && declaration.socialMedia !== undefined
200
+ && declaration.socialMedia !== expected.socialMedia) {
201
+ risks.push({
202
+ level: 'blocker',
203
+ code: 'SOCIAL_MEDIA_CAPABILITY_MISMATCH',
204
+ title: '앱 기능과 App Store 소셜 미디어 응답 불일치',
205
+ detail: `프로젝트 기능 선언은 socialMedia=${expected.socialMedia}이지만 App Store Connect는 ${String(declaration.socialMedia)}입니다.`,
206
+ });
207
+ }
208
+ if (expected.socialMediaAgeRestricted !== undefined
209
+ && declaration.socialMediaAgeRestricted !== undefined
210
+ && declaration.socialMediaAgeRestricted !== expected.socialMediaAgeRestricted) {
211
+ risks.push({
212
+ level: 'blocker',
213
+ code: 'SOCIAL_MEDIA_AGE_GATE_MISMATCH',
214
+ title: '13세 미만 기능 제한과 App Store 응답 불일치',
215
+ detail: `프로젝트 기능 선언은 socialMediaAgeRestricted=${expected.socialMediaAgeRestricted}이지만 App Store Connect는 ${String(declaration.socialMediaAgeRestricted)}입니다.`,
216
+ });
217
+ }
218
+ if (declaration.userGeneratedContent && declaration.socialMedia === false) {
219
+ risks.push({
220
+ level: 'warning',
221
+ code: 'UGC_WITHOUT_SOCIAL_MEDIA',
222
+ title: 'UGC는 있으나 소셜 미디어 기능은 없음으로 응답',
223
+ detail: '피드·좋아요·댓글·공유·발견 기능이 있다면 socialMedia 응답을 true로 바꿔야 합니다.',
224
+ });
225
+ }
226
+ }
154
227
  }
155
228
  }
156
229
  return risks;
@@ -0,0 +1,163 @@
1
+ import type { OAuth2Client } from 'google-auth-library';
2
+ interface OptionalResult<T> {
3
+ available: boolean;
4
+ data?: T;
5
+ error?: string;
6
+ }
7
+ interface RemoteConfigTemplate {
8
+ parameters?: Record<string, unknown>;
9
+ parameterGroups?: Record<string, unknown>;
10
+ conditions?: unknown[];
11
+ version?: {
12
+ versionNumber?: string;
13
+ updateTime?: string;
14
+ updateUser?: {
15
+ email?: string;
16
+ };
17
+ updateOrigin?: string;
18
+ updateType?: string;
19
+ };
20
+ }
21
+ interface PagedItems<T> {
22
+ items: T[];
23
+ pages: number;
24
+ truncated: boolean;
25
+ }
26
+ export declare function countStates(items: Array<{
27
+ state?: string;
28
+ }>): Record<string, number>;
29
+ export declare function estimateRemoteConfigDailyCost(fetches: number): number;
30
+ export declare function remoteConfigUsageLevel(fetches: number): 'ok' | 'warning' | 'critical';
31
+ export declare function getRemoteConfigOverview(auth: OAuth2Client, input: {
32
+ projectId: string;
33
+ namespace?: string;
34
+ days?: number;
35
+ }): Promise<{
36
+ projectId: string;
37
+ namespace: string;
38
+ pricingPolicy: {
39
+ effectiveDate: string;
40
+ noCostDailyFetches: number;
41
+ firstPaidTier: {
42
+ from: number;
43
+ to: number;
44
+ usdPerRequest: number;
45
+ };
46
+ secondPaidTier: {
47
+ from: number;
48
+ usdPerRequest: number;
49
+ };
50
+ transitionNotice: string;
51
+ sourceUrl: string;
52
+ };
53
+ billing: {
54
+ readonly available: false;
55
+ readonly error: string;
56
+ } | {
57
+ available: boolean;
58
+ plan: string;
59
+ billingEnabled: boolean;
60
+ };
61
+ usage: OptionalResult<{
62
+ date: string;
63
+ fetches: number;
64
+ }[]> | {
65
+ available: boolean;
66
+ metric: string;
67
+ days: {
68
+ date: string;
69
+ fetches: number;
70
+ }[];
71
+ latest: {
72
+ utilizationPercent: number;
73
+ level: "ok" | "warning" | "critical";
74
+ projectedStandardDailyCostUsd: number;
75
+ date: string;
76
+ fetches: number;
77
+ } | null;
78
+ peak: {
79
+ utilizationPercent: number;
80
+ level: "ok" | "warning" | "critical";
81
+ projectedStandardDailyCostUsd: number;
82
+ date: string;
83
+ fetches: number;
84
+ } | null;
85
+ };
86
+ template: OptionalResult<RemoteConfigTemplate> | {
87
+ available: boolean;
88
+ version: {
89
+ versionNumber?: string;
90
+ updateTime?: string;
91
+ updateUser?: {
92
+ email?: string;
93
+ };
94
+ updateOrigin?: string;
95
+ updateType?: string;
96
+ } | null;
97
+ parameterCount: number;
98
+ parameterGroupCount: number;
99
+ conditionCount: number;
100
+ };
101
+ experiments: OptionalResult<PagedItems<{
102
+ name?: string;
103
+ state?: string;
104
+ startTime?: string;
105
+ endTime?: string;
106
+ lastUpdateTime?: string;
107
+ definition?: unknown;
108
+ }>> | {
109
+ available: boolean;
110
+ total: number;
111
+ stateCounts: Record<string, number>;
112
+ active: {
113
+ name: string | undefined;
114
+ displayName: string | undefined;
115
+ state: string | undefined;
116
+ startTime: string | undefined;
117
+ endTime: string | undefined;
118
+ lastUpdateTime: string | undefined;
119
+ }[];
120
+ recent: {
121
+ name: string | undefined;
122
+ displayName: string | undefined;
123
+ state: string | undefined;
124
+ startTime: string | undefined;
125
+ endTime: string | undefined;
126
+ lastUpdateTime: string | undefined;
127
+ }[];
128
+ pages: number;
129
+ truncated: boolean;
130
+ };
131
+ rollouts: OptionalResult<PagedItems<{
132
+ name?: string;
133
+ state?: string;
134
+ startTime?: string;
135
+ endTime?: string;
136
+ lastUpdateTime?: string;
137
+ definition?: unknown;
138
+ }>> | {
139
+ available: boolean;
140
+ total: number;
141
+ stateCounts: Record<string, number>;
142
+ active: {
143
+ name: string | undefined;
144
+ displayName: string | undefined;
145
+ state: string | undefined;
146
+ startTime: string | undefined;
147
+ endTime: string | undefined;
148
+ lastUpdateTime: string | undefined;
149
+ }[];
150
+ recent: {
151
+ name: string | undefined;
152
+ displayName: string | undefined;
153
+ state: string | undefined;
154
+ startTime: string | undefined;
155
+ endTime: string | undefined;
156
+ lastUpdateTime: string | undefined;
157
+ }[];
158
+ pages: number;
159
+ truncated: boolean;
160
+ };
161
+ warnings: string[];
162
+ }>;
163
+ export {};