@@ -27,6 +27,8 @@ const mockPatchInstanceConfig = mock();
2727const mockFetchInstanceConfig = mock ( ) ;
2828const mockFetchInstanceConfigSchema = mock ( ) ;
2929const mockFetchApplication = mock ( ) ;
30+ const mockListIOSApplications = mock ( ) ;
31+ const mockGetNativeSettings = mock ( ) ;
3032const mockListApplicationDomains = mock ( ) ;
3133const mockCreateProductionInstance = mock ( ) ;
3234const mockGetApplicationDomainStatus = mock ( ) ;
@@ -49,6 +51,8 @@ mock.module("../../lib/plapi.ts", () => ({
4951 fetchInstanceConfig : ( ...args : unknown [ ] ) => mockFetchInstanceConfig ( ...args ) ,
5052 fetchInstanceConfigSchema : ( ...args : unknown [ ] ) => mockFetchInstanceConfigSchema ( ...args ) ,
5153 fetchApplication : ( ...args : unknown [ ] ) => mockFetchApplication ( ...args ) ,
54+ listIOSApplications : ( ...args : unknown [ ] ) => mockListIOSApplications ( ...args ) ,
55+ getNativeSettings : ( ...args : unknown [ ] ) => mockGetNativeSettings ( ...args ) ,
5256 listApplicationDomains : ( ...args : unknown [ ] ) => mockListApplicationDomains ( ...args ) ,
5357 createProductionInstance : ( ...args : unknown [ ] ) => mockCreateProductionInstance ( ...args ) ,
5458 getApplicationDomainStatus : ( ...args : unknown [ ] ) => mockGetApplicationDomainStatus ( ...args ) ,
@@ -228,6 +232,8 @@ describe("deploy", () => {
228232 mockGetApplicationDomainStatus . mockResolvedValue (
229233 domainStatus ( { status : "complete" , dns : true , ssl : true , mail : true } ) ,
230234 ) ;
235+ mockListIOSApplications . mockResolvedValue ( [ ] ) ;
236+ mockGetNativeSettings . mockResolvedValue ( { object : "native_settings" , api_enabled : true } ) ;
231237 stubCreateProductionInstance ( ) ;
232238 mockTriggerApplicationDomainDNSCheck . mockResolvedValue (
233239 domainStatus ( { status : "complete" , dns : true , ssl : true , mail : true } ) ,
@@ -261,6 +267,8 @@ describe("deploy", () => {
261267 mockFetchInstanceConfig . mockReset ( ) ;
262268 mockFetchInstanceConfigSchema . mockReset ( ) ;
263269 mockFetchApplication . mockReset ( ) ;
270+ mockListIOSApplications . mockReset ( ) ;
271+ mockGetNativeSettings . mockReset ( ) ;
264272 mockListApplicationDomains . mockReset ( ) ;
265273 mockCreateProductionInstance . mockReset ( ) ;
266274 mockGetApplicationDomainStatus . mockReset ( ) ;
@@ -1253,6 +1261,213 @@ describe("deploy", () => {
12531261 expect ( err ) . not . toContain ( "https://accounts.example.com/v1/oauth_callback" ) ;
12541262 } ) ;
12551263
1264+ test ( "skips Apple web credential prompts for an exact native-only production registration" , async ( ) => {
1265+ await linkedProject ( {
1266+ instances : { development : "ins_dev_123" , production : "ins_prod_native_apple" } ,
1267+ } ) ;
1268+ mockLiveProduction ( {
1269+ instanceId : "ins_prod_native_apple" ,
1270+ developmentConfig : {
1271+ connection_oauth_apple : {
1272+ enabled : true ,
1273+ authenticatable : true ,
1274+ bundle_id : "com.example.native" ,
1275+ } ,
1276+ } ,
1277+ productionConfig : {
1278+ connection_oauth_apple : {
1279+ enabled : true ,
1280+ authenticatable : true ,
1281+ bundle_id : "com.example.native" ,
1282+ } ,
1283+ } ,
1284+ } ) ;
1285+ mockListIOSApplications . mockResolvedValueOnce ( [
1286+ {
1287+ object : "ios_application" ,
1288+ id : "ios_native" ,
1289+ app_id_prefix : "ABCDE12345" ,
1290+ bundle_id : "com.example.native" ,
1291+ created_at : 1 ,
1292+ updated_at : 1 ,
1293+ } ,
1294+ ] ) ;
1295+ mockIsAgent . mockReturnValue ( false ) ;
1296+
1297+ await runDeploy ( { } ) ;
1298+
1299+ expect ( mockListIOSApplications ) . toHaveBeenCalledWith ( "app_xyz789" , "ins_prod_native_apple" ) ;
1300+ expect ( mockGetNativeSettings ) . toHaveBeenCalledWith ( "app_xyz789" , "ins_prod_native_apple" ) ;
1301+ expect ( mockSelect ) . not . toHaveBeenCalled ( ) ;
1302+ expect ( mockInput ) . not . toHaveBeenCalled ( ) ;
1303+ expect ( mockPassword ) . not . toHaveBeenCalled ( ) ;
1304+ expect ( mockPatchInstanceConfig ) . not . toHaveBeenCalled ( ) ;
1305+ const err = stripAnsi ( captured . err ) ;
1306+ expect ( err ) . toContain ( "No deploy actions remain." ) ;
1307+ expect ( err ) . toContain ( "OAuth Apple" ) ;
1308+ expect ( err ) . not . toContain ( "Configure Apple OAuth for production" ) ;
1309+ } ) ;
1310+
1311+ test ( "refuses to infer an App ID Prefix when native Apple lacks an exact production registration" , async ( ) => {
1312+ await linkedProject ( {
1313+ instances : { development : "ins_dev_123" , production : "ins_prod_native_apple" } ,
1314+ } ) ;
1315+ mockLiveProduction ( {
1316+ instanceId : "ins_prod_native_apple" ,
1317+ developmentConfig : {
1318+ connection_oauth_apple : {
1319+ enabled : true ,
1320+ authenticatable : true ,
1321+ bundle_id : "com.example.native" ,
1322+ } ,
1323+ } ,
1324+ productionConfig : {
1325+ connection_oauth_apple : {
1326+ enabled : true ,
1327+ authenticatable : true ,
1328+ bundle_id : "com.example.native" ,
1329+ } ,
1330+ } ,
1331+ } ) ;
1332+ mockListIOSApplications . mockResolvedValueOnce ( [
1333+ {
1334+ object : "ios_application" ,
1335+ id : "ios_other" ,
1336+ app_id_prefix : "OTHER12345" ,
1337+ bundle_id : "com.example.other" ,
1338+ created_at : 1 ,
1339+ updated_at : 1 ,
1340+ } ,
1341+ ] ) ;
1342+ mockIsAgent . mockReturnValue ( false ) ;
1343+
1344+ await expect ( runDeploy ( { } ) ) . rejects . toThrow (
1345+ "the production instance does not have an exact iOS Native Application registration for that Bundle ID" ,
1346+ ) ;
1347+
1348+ expect ( mockSelect ) . not . toHaveBeenCalled ( ) ;
1349+ expect ( mockInput ) . not . toHaveBeenCalled ( ) ;
1350+ expect ( mockPassword ) . not . toHaveBeenCalled ( ) ;
1351+ expect ( mockPatchInstanceConfig ) . not . toHaveBeenCalled ( ) ;
1352+ expect ( stripAnsi ( captured . err ) ) . toContain ( "Failed" ) ;
1353+ } ) ;
1354+
1355+ test ( "preserves Ctrl-C while verifying a native-only Apple registration" , async ( ) => {
1356+ await linkedProject ( {
1357+ instances : { development : "ins_dev_123" , production : "ins_prod_native_apple" } ,
1358+ } ) ;
1359+ mockLiveProduction ( {
1360+ instanceId : "ins_prod_native_apple" ,
1361+ developmentConfig : {
1362+ connection_oauth_apple : {
1363+ enabled : true ,
1364+ authenticatable : true ,
1365+ bundle_id : "com.example.native" ,
1366+ } ,
1367+ } ,
1368+ productionConfig : {
1369+ connection_oauth_apple : {
1370+ enabled : true ,
1371+ authenticatable : true ,
1372+ bundle_id : "com.example.native" ,
1373+ } ,
1374+ } ,
1375+ } ) ;
1376+ mockListIOSApplications
1377+ . mockRejectedValueOnce ( new Error ( "native status endpoint unavailable" ) )
1378+ . mockRejectedValueOnce ( promptExitError ( ) ) ;
1379+ mockIsAgent . mockReturnValue ( false ) ;
1380+
1381+ await expect ( runDeploy ( { } ) ) . rejects . toMatchObject ( { exitCode : EXIT_CODE . SIGINT } ) ;
1382+
1383+ expect ( mockSelect ) . not . toHaveBeenCalled ( ) ;
1384+ expect ( mockInput ) . not . toHaveBeenCalled ( ) ;
1385+ expect ( mockPassword ) . not . toHaveBeenCalled ( ) ;
1386+ expect ( mockPatchInstanceConfig ) . not . toHaveBeenCalled ( ) ;
1387+ expect ( stripAnsi ( captured . err ) ) . toContain ( "Paused" ) ;
1388+ } ) ;
1389+
1390+ test ( "refuses native-only Apple when production Native API is disabled" , async ( ) => {
1391+ await linkedProject ( {
1392+ instances : { development : "ins_dev_123" , production : "ins_prod_native_apple" } ,
1393+ } ) ;
1394+ mockLiveProduction ( {
1395+ instanceId : "ins_prod_native_apple" ,
1396+ developmentConfig : {
1397+ connection_oauth_apple : {
1398+ enabled : true ,
1399+ authenticatable : true ,
1400+ bundle_id : "com.example.native" ,
1401+ } ,
1402+ } ,
1403+ productionConfig : {
1404+ connection_oauth_apple : {
1405+ enabled : true ,
1406+ authenticatable : true ,
1407+ bundle_id : "com.example.native" ,
1408+ } ,
1409+ } ,
1410+ } ) ;
1411+ mockListIOSApplications . mockResolvedValue ( [
1412+ {
1413+ object : "ios_application" ,
1414+ id : "ios_native" ,
1415+ app_id_prefix : "ABCDE12345" ,
1416+ bundle_id : "com.example.native" ,
1417+ created_at : 1 ,
1418+ updated_at : 1 ,
1419+ } ,
1420+ ] ) ;
1421+ mockGetNativeSettings . mockResolvedValue ( {
1422+ object : "native_settings" ,
1423+ api_enabled : false ,
1424+ } ) ;
1425+ mockIsAgent . mockReturnValue ( false ) ;
1426+
1427+ await expect ( runDeploy ( { } ) ) . rejects . toThrow (
1428+ "Enable Native API at https://dashboard.clerk.com/~/native-applications" ,
1429+ ) ;
1430+
1431+ expect ( mockSelect ) . not . toHaveBeenCalled ( ) ;
1432+ expect ( mockInput ) . not . toHaveBeenCalled ( ) ;
1433+ expect ( mockPassword ) . not . toHaveBeenCalled ( ) ;
1434+ expect ( mockPatchInstanceConfig ) . not . toHaveBeenCalled ( ) ;
1435+ } ) ;
1436+
1437+ test ( "refuses native-only Apple that is not explicitly authenticatable" , async ( ) => {
1438+ await linkedProject ( {
1439+ instances : { development : "ins_dev_123" , production : "ins_prod_native_apple" } ,
1440+ } ) ;
1441+ mockLiveProduction ( {
1442+ instanceId : "ins_prod_native_apple" ,
1443+ developmentConfig : {
1444+ connection_oauth_apple : {
1445+ enabled : true ,
1446+ authenticatable : true ,
1447+ bundle_id : "com.example.native" ,
1448+ } ,
1449+ } ,
1450+ productionConfig : {
1451+ connection_oauth_apple : {
1452+ enabled : true ,
1453+ bundle_id : "com.example.native" ,
1454+ } ,
1455+ } ,
1456+ } ) ;
1457+ mockIsAgent . mockReturnValue ( false ) ;
1458+
1459+ await expect ( runDeploy ( { } ) ) . rejects . toThrow (
1460+ "Apple is not explicitly enabled for authentication on the production instance" ,
1461+ ) ;
1462+
1463+ expect ( mockListIOSApplications ) . not . toHaveBeenCalled ( ) ;
1464+ expect ( mockGetNativeSettings ) . not . toHaveBeenCalled ( ) ;
1465+ expect ( mockSelect ) . not . toHaveBeenCalled ( ) ;
1466+ expect ( mockInput ) . not . toHaveBeenCalled ( ) ;
1467+ expect ( mockPassword ) . not . toHaveBeenCalled ( ) ;
1468+ expect ( mockPatchInstanceConfig ) . not . toHaveBeenCalled ( ) ;
1469+ } ) ;
1470+
12561471 test ( "Apple .p8 file prompt validates path and PEM framing before continuing" , async ( ) => {
12571472 await linkedProject ( {
12581473 instances : { development : "ins_dev_123" , production : "ins_prod_apple" } ,
@@ -1306,6 +1521,8 @@ describe("deploy", () => {
13061521 "-----BEGIN PRIVATE KEY-----\nMIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQg\n-----END PRIVATE KEY-----\n" ,
13071522 } ,
13081523 } ) ;
1524+ expect ( mockListIOSApplications ) . not . toHaveBeenCalled ( ) ;
1525+ expect ( mockGetNativeSettings ) . not . toHaveBeenCalled ( ) ;
13091526 const p8Input = mockInput . mock . calls . find ( ( call ) =>
13101527 String ( ( call [ 0 ] as { message ?: string } ) . message ) . includes ( "Apple Private Key" ) ,
13111528 ) ?. [ 0 ] as { validate : ( value : string ) => Promise < true | string > } ;
0 commit comments