403 lines
15 KiB
JavaScript
403 lines
15 KiB
JavaScript
import { readFile } from 'node:fs/promises'
|
|
import { resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = resolve(import.meta.dirname, '../..')
|
|
const DEFAULT_GOOGLE_SERVICES_PATH = resolve(
|
|
ROOT,
|
|
'apps/mobile-rn/android/app/google-services.json',
|
|
)
|
|
const PRODUCTION_PACKAGE_NAME = 'com.d3ro.voice'
|
|
const GOOGLE_TEST_ADMOB_PUBLISHER_ID = '3940256099942544'
|
|
const FIREBASE_PROJECT_ID = /^[a-z][a-z0-9-]{4,28}[a-z0-9]$/
|
|
const FIREBASE_PROJECT_NUMBER = /^[1-9][0-9]{5,19}$/
|
|
const FIREBASE_MOBILESDK_APP_ID = /^1:([1-9][0-9]{5,19}):android:([0-9a-f]{16,64})$/
|
|
const ANDROID_PACKAGE_NAME = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/i
|
|
const GOOGLE_API_KEY = /^AIza[0-9A-Za-z_-]{20,96}$/
|
|
const ADMOB_APP_ID = /^ca-app-pub-([0-9]{16})~([0-9]{10})$/
|
|
const ADMOB_UNIT_ID = /^ca-app-pub-([0-9]{16})\/([0-9]{10})$/
|
|
const MAX_GOOGLE_SERVICES_BYTES = 1024 * 1024
|
|
|
|
function fail(code) {
|
|
throw new Error(`mobile_release_config_invalid:${code}`)
|
|
}
|
|
|
|
function isRecord(value) {
|
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
}
|
|
|
|
function requireExactString(value, code) {
|
|
if (typeof value !== 'string' || value.length === 0 || value !== value.trim()) fail(code)
|
|
return value
|
|
}
|
|
|
|
function requireExpectedFirebaseIdentity(expected) {
|
|
if (!isRecord(expected)) fail('firebase_expected_identity_missing')
|
|
|
|
const projectId = requireExactString(
|
|
expected.projectId,
|
|
'firebase_expected_project_id_missing',
|
|
)
|
|
const projectNumber = requireExactString(
|
|
expected.projectNumber,
|
|
'firebase_expected_project_number_missing',
|
|
)
|
|
const mobileSdkAppId = requireExactString(
|
|
expected.mobileSdkAppId,
|
|
'firebase_expected_mobilesdk_app_id_missing',
|
|
)
|
|
|
|
if (!FIREBASE_PROJECT_ID.test(projectId)) fail('firebase_expected_project_id_format')
|
|
if (!FIREBASE_PROJECT_NUMBER.test(projectNumber)) {
|
|
fail('firebase_expected_project_number_format')
|
|
}
|
|
const appIdMatch = mobileSdkAppId.match(FIREBASE_MOBILESDK_APP_ID)
|
|
if (!appIdMatch) fail('firebase_expected_mobilesdk_app_id_format')
|
|
if (appIdMatch[1] !== projectNumber) {
|
|
fail('firebase_expected_mobilesdk_app_id_project_number_mismatch')
|
|
}
|
|
|
|
return { projectId, projectNumber, mobileSdkAppId }
|
|
}
|
|
|
|
export function parseGoogleServicesSource(source) {
|
|
if (typeof source !== 'string' || source.length === 0) fail('google_services_empty')
|
|
if (Buffer.byteLength(source, 'utf8') > MAX_GOOGLE_SERVICES_BYTES) {
|
|
fail('google_services_too_large')
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(source)
|
|
} catch {
|
|
fail('google_services_invalid_json')
|
|
}
|
|
}
|
|
|
|
export function verifyFirebaseConfiguration(configuration, expectedIdentity) {
|
|
const expected = requireExpectedFirebaseIdentity(expectedIdentity)
|
|
if (!isRecord(configuration)) fail('google_services_root_invalid')
|
|
if (configuration.configuration_version !== '1') {
|
|
fail('google_services_configuration_version_invalid')
|
|
}
|
|
|
|
const projectInfo = configuration.project_info
|
|
if (!isRecord(projectInfo)) fail('firebase_project_info_missing')
|
|
const projectId = requireExactString(projectInfo.project_id, 'firebase_project_id_missing')
|
|
const projectNumber = requireExactString(
|
|
projectInfo.project_number,
|
|
'firebase_project_number_missing',
|
|
)
|
|
if (!FIREBASE_PROJECT_ID.test(projectId)) fail('firebase_project_id_format')
|
|
if (!FIREBASE_PROJECT_NUMBER.test(projectNumber)) fail('firebase_project_number_format')
|
|
if (projectId !== expected.projectId) fail('firebase_project_id_mismatch')
|
|
if (projectNumber !== expected.projectNumber) fail('firebase_project_number_mismatch')
|
|
|
|
const clients = configuration.client
|
|
if (!Array.isArray(clients) || clients.length === 0) fail('firebase_clients_missing')
|
|
if (clients.length > 100) fail('firebase_client_count_excessive')
|
|
|
|
const packages = new Set()
|
|
const appIds = new Set()
|
|
let productionClient
|
|
|
|
for (const client of clients) {
|
|
if (!isRecord(client)) fail('firebase_client_invalid')
|
|
const clientInfo = client.client_info
|
|
if (!isRecord(clientInfo)) fail('firebase_client_info_missing')
|
|
const androidClientInfo = clientInfo.android_client_info
|
|
if (!isRecord(androidClientInfo)) fail('firebase_android_client_info_missing')
|
|
|
|
const packageName = requireExactString(
|
|
androidClientInfo.package_name,
|
|
'firebase_client_package_missing',
|
|
)
|
|
const mobileSdkAppId = requireExactString(
|
|
clientInfo.mobilesdk_app_id,
|
|
'firebase_client_mobilesdk_app_id_missing',
|
|
)
|
|
if (!ANDROID_PACKAGE_NAME.test(packageName)) fail('firebase_client_package_format')
|
|
const appIdMatch = mobileSdkAppId.match(FIREBASE_MOBILESDK_APP_ID)
|
|
if (!appIdMatch) fail('firebase_client_mobilesdk_app_id_format')
|
|
if (appIdMatch[1] !== projectNumber) {
|
|
fail('firebase_client_mobilesdk_app_id_project_number_mismatch')
|
|
}
|
|
if (packages.has(packageName)) fail('firebase_client_package_duplicate')
|
|
if (appIds.has(mobileSdkAppId)) fail('firebase_client_mobilesdk_app_id_duplicate')
|
|
packages.add(packageName)
|
|
appIds.add(mobileSdkAppId)
|
|
|
|
if (packageName === PRODUCTION_PACKAGE_NAME) productionClient = client
|
|
}
|
|
|
|
if (!productionClient) fail('firebase_production_client_missing')
|
|
if (productionClient.client_info.mobilesdk_app_id !== expected.mobileSdkAppId) {
|
|
fail('firebase_mobilesdk_app_id_mismatch')
|
|
}
|
|
|
|
const apiKeys = productionClient.api_key
|
|
if (!Array.isArray(apiKeys) || apiKeys.length !== 1) {
|
|
fail('firebase_production_api_key_must_be_unique')
|
|
}
|
|
if (!isRecord(apiKeys[0])) fail('firebase_production_api_key_invalid')
|
|
const apiKey = requireExactString(
|
|
apiKeys[0].current_key,
|
|
'firebase_production_api_key_missing',
|
|
)
|
|
if (!GOOGLE_API_KEY.test(apiKey)) fail('firebase_production_api_key_format')
|
|
|
|
return Object.freeze({
|
|
packageNameVerified: true,
|
|
projectIdentityVerified: true,
|
|
clientIdentityUnique: true,
|
|
apiKeyUnique: true,
|
|
})
|
|
}
|
|
|
|
function parseAdMobId(value, expression, missingCode, formatCode) {
|
|
const normalized = requireExactString(value, missingCode)
|
|
const match = normalized.match(expression)
|
|
if (!match) fail(formatCode)
|
|
if (/^0+$/.test(match[1]) || /^0+$/.test(match[2])) fail(formatCode)
|
|
return { value: normalized, publisherId: match[1] }
|
|
}
|
|
|
|
export function verifyAdMobConfiguration(configuration) {
|
|
if (!isRecord(configuration)) fail('admob_configuration_missing')
|
|
const app = parseAdMobId(
|
|
configuration.appId,
|
|
ADMOB_APP_ID,
|
|
'admob_app_id_missing',
|
|
'admob_app_id_format',
|
|
)
|
|
const banner = parseAdMobId(
|
|
configuration.bannerUnitId,
|
|
ADMOB_UNIT_ID,
|
|
'admob_banner_unit_id_missing',
|
|
'admob_banner_unit_id_format',
|
|
)
|
|
const rewarded = parseAdMobId(
|
|
configuration.rewardedUnitId,
|
|
ADMOB_UNIT_ID,
|
|
'admob_rewarded_unit_id_missing',
|
|
'admob_rewarded_unit_id_format',
|
|
)
|
|
|
|
for (const entry of [app, banner, rewarded]) {
|
|
if (entry.publisherId === GOOGLE_TEST_ADMOB_PUBLISHER_ID) {
|
|
fail('admob_google_test_publisher_rejected')
|
|
}
|
|
}
|
|
if (app.publisherId !== banner.publisherId || app.publisherId !== rewarded.publisherId) {
|
|
fail('admob_publisher_mismatch')
|
|
}
|
|
if (banner.value === rewarded.value) fail('admob_unit_ids_must_be_unique')
|
|
|
|
return Object.freeze({
|
|
productionFormatVerified: true,
|
|
googleTestPublisherRejected: true,
|
|
publisherConsistencyVerified: true,
|
|
unitIdentityUnique: true,
|
|
})
|
|
}
|
|
|
|
function clone(value) {
|
|
return structuredClone(value)
|
|
}
|
|
|
|
function expectFailure(label, action, expectedCode) {
|
|
try {
|
|
action()
|
|
} catch (error) {
|
|
const expectedMessage = `mobile_release_config_invalid:${expectedCode}`
|
|
if (error instanceof Error && error.message === expectedMessage) return
|
|
throw new Error(`mobile_release_config_self_test_unexpected:${label}`)
|
|
}
|
|
throw new Error(`mobile_release_config_self_test_missed:${label}`)
|
|
}
|
|
|
|
function makeFirebaseFixture() {
|
|
return {
|
|
configuration_version: '1',
|
|
project_info: {
|
|
project_number: '123456789012',
|
|
project_id: 'd3ro-production',
|
|
storage_bucket: 'd3ro-production.example.invalid',
|
|
},
|
|
client: [
|
|
{
|
|
client_info: {
|
|
mobilesdk_app_id: '1:123456789012:android:0123456789abcdef',
|
|
android_client_info: { package_name: PRODUCTION_PACKAGE_NAME },
|
|
},
|
|
api_key: [
|
|
{ current_key: ['AI', 'zaFixtureKeyMaterial1234567890abcd'].join('') },
|
|
],
|
|
},
|
|
],
|
|
}
|
|
}
|
|
|
|
function runSelfTest() {
|
|
const expected = {
|
|
projectId: 'd3ro-production',
|
|
projectNumber: '123456789012',
|
|
mobileSdkAppId: '1:123456789012:android:0123456789abcdef',
|
|
}
|
|
const firebase = makeFirebaseFixture()
|
|
const admob = {
|
|
appId: 'ca-app-pub-1234567890123456~1234567890',
|
|
bannerUnitId: 'ca-app-pub-1234567890123456/2345678901',
|
|
rewardedUnitId: 'ca-app-pub-1234567890123456/3456789012',
|
|
}
|
|
|
|
verifyFirebaseConfiguration(firebase, expected)
|
|
verifyAdMobConfiguration(admob)
|
|
parseGoogleServicesSource(JSON.stringify(firebase))
|
|
|
|
const firebaseCases = [
|
|
['invalid_json', () => parseGoogleServicesSource('{'), 'google_services_invalid_json'],
|
|
['root_array', () => verifyFirebaseConfiguration([], expected), 'google_services_root_invalid'],
|
|
['configuration_version', () => {
|
|
const value = clone(firebase)
|
|
value.configuration_version = '2'
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'google_services_configuration_version_invalid'],
|
|
['project_id_mismatch', () => {
|
|
const value = clone(firebase)
|
|
value.project_info.project_id = 'different-production'
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'firebase_project_id_mismatch'],
|
|
['project_number_mismatch', () => {
|
|
const value = clone(firebase)
|
|
value.project_info.project_number = '223456789012'
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'firebase_project_number_mismatch'],
|
|
['missing_clients', () => {
|
|
const value = clone(firebase)
|
|
value.client = []
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'firebase_clients_missing'],
|
|
['duplicate_package', () => {
|
|
const value = clone(firebase)
|
|
const duplicate = clone(value.client[0])
|
|
duplicate.client_info.mobilesdk_app_id = '1:123456789012:android:fedcba9876543210'
|
|
value.client.push(duplicate)
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'firebase_client_package_duplicate'],
|
|
['duplicate_app_id', () => {
|
|
const value = clone(firebase)
|
|
const duplicate = clone(value.client[0])
|
|
duplicate.client_info.android_client_info.package_name = 'com.d3ro.voice.other'
|
|
value.client.push(duplicate)
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'firebase_client_mobilesdk_app_id_duplicate'],
|
|
['production_package_missing', () => {
|
|
const value = clone(firebase)
|
|
value.client[0].client_info.android_client_info.package_name = 'com.d3ro.voice.other'
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'firebase_production_client_missing'],
|
|
['app_id_mismatch', () => {
|
|
const value = clone(firebase)
|
|
value.client[0].client_info.mobilesdk_app_id = '1:123456789012:android:fedcba9876543210'
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'firebase_mobilesdk_app_id_mismatch'],
|
|
['app_id_project_number_mismatch', () => {
|
|
const value = clone(firebase)
|
|
value.client[0].client_info.mobilesdk_app_id = '1:223456789012:android:0123456789abcdef'
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'firebase_client_mobilesdk_app_id_project_number_mismatch'],
|
|
['api_key_missing', () => {
|
|
const value = clone(firebase)
|
|
value.client[0].api_key = []
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'firebase_production_api_key_must_be_unique'],
|
|
['api_key_ambiguous', () => {
|
|
const value = clone(firebase)
|
|
value.client[0].api_key.push(clone(value.client[0].api_key[0]))
|
|
return verifyFirebaseConfiguration(value, expected)
|
|
}, 'firebase_production_api_key_must_be_unique'],
|
|
['expected_identity_missing', () => verifyFirebaseConfiguration(firebase, {}), 'firebase_expected_project_id_missing'],
|
|
['expected_app_id_project_number_mismatch', () => verifyFirebaseConfiguration(firebase, {
|
|
...expected,
|
|
mobileSdkAppId: '1:223456789012:android:0123456789abcdef',
|
|
}), 'firebase_expected_mobilesdk_app_id_project_number_mismatch'],
|
|
]
|
|
for (const [label, action, expectedCode] of firebaseCases) {
|
|
expectFailure(label, action, expectedCode)
|
|
}
|
|
|
|
const admobCases = [
|
|
['app_format', { ...admob, appId: 'ca-app-pub-123~456' }, 'admob_app_id_format'],
|
|
['banner_format', { ...admob, bannerUnitId: 'ca-app-pub-1234567890123456~2345678901' }, 'admob_banner_unit_id_format'],
|
|
['rewarded_format', { ...admob, rewardedUnitId: 'ca-app-pub-1234567890123456/123' }, 'admob_rewarded_unit_id_format'],
|
|
['app_test_publisher', {
|
|
appId: 'ca-app-pub-3940256099942544~3347511713',
|
|
bannerUnitId: 'ca-app-pub-3940256099942544/6300978111',
|
|
rewardedUnitId: 'ca-app-pub-3940256099942544/5224354917',
|
|
}, 'admob_google_test_publisher_rejected'],
|
|
['banner_test_publisher', {
|
|
...admob,
|
|
bannerUnitId: 'ca-app-pub-3940256099942544/6300978111',
|
|
}, 'admob_google_test_publisher_rejected'],
|
|
['publisher_mismatch', {
|
|
...admob,
|
|
rewardedUnitId: 'ca-app-pub-2234567890123456/3456789012',
|
|
}, 'admob_publisher_mismatch'],
|
|
['unit_duplicate', { ...admob, rewardedUnitId: admob.bannerUnitId }, 'admob_unit_ids_must_be_unique'],
|
|
['whitespace', { ...admob, appId: `${admob.appId} ` }, 'admob_app_id_missing'],
|
|
['zero_identifier', {
|
|
...admob,
|
|
bannerUnitId: 'ca-app-pub-1234567890123456/0000000000',
|
|
}, 'admob_banner_unit_id_format'],
|
|
]
|
|
for (const [label, configuration, expectedCode] of admobCases) {
|
|
expectFailure(label, () => verifyAdMobConfiguration(configuration), expectedCode)
|
|
}
|
|
|
|
return { firebaseFailureCases: firebaseCases.length, adMobFailureCases: admobCases.length }
|
|
}
|
|
|
|
async function run() {
|
|
if (process.argv.includes('--self-test')) {
|
|
const result = runSelfTest()
|
|
console.log(JSON.stringify({ selfTest: 'verified', ...result }))
|
|
return
|
|
}
|
|
if (process.argv.length > 2) fail('unknown_argument')
|
|
|
|
let source
|
|
try {
|
|
source = await readFile(
|
|
process.env.D3RO_GOOGLE_SERVICES_JSON_FILE || DEFAULT_GOOGLE_SERVICES_PATH,
|
|
'utf8',
|
|
)
|
|
} catch {
|
|
fail('google_services_file_unreadable')
|
|
}
|
|
|
|
const firebase = verifyFirebaseConfiguration(parseGoogleServicesSource(source), {
|
|
projectId: process.env.D3RO_FIREBASE_EXPECTED_PROJECT_ID,
|
|
projectNumber: process.env.D3RO_FIREBASE_EXPECTED_PROJECT_NUMBER,
|
|
mobileSdkAppId: process.env.D3RO_FIREBASE_EXPECTED_MOBILESDK_APP_ID,
|
|
})
|
|
const adMob = verifyAdMobConfiguration({
|
|
appId: process.env.D3RO_ADMOB_APP_ID,
|
|
bannerUnitId: process.env.D3RO_ADMOB_BANNER_UNIT_ID,
|
|
rewardedUnitId: process.env.D3RO_ADMOB_REWARDED_UNIT_ID,
|
|
})
|
|
|
|
console.log(JSON.stringify({
|
|
releaseConfiguration: 'verified',
|
|
firebase,
|
|
adMob,
|
|
}))
|
|
}
|
|
|
|
const isMain = process.argv[1]
|
|
&& resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))
|
|
if (isMain) {
|
|
run().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : 'mobile_release_config_invalid:unknown')
|
|
process.exitCode = 1
|
|
})
|
|
}
|