Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,11 @@ jobs:
yarn build:android

build-ios:
# Vendored TalsecRuntime fallback (FREERASP_DISABLE_SPM=1).
runs-on: macos-latest
needs: build-library
env:
FREERASP_DISABLE_SPM: "1"
steps:
- name: Checkout
uses: actions/checkout@v6
Expand All @@ -99,3 +102,25 @@ jobs:
working-directory: ./example
run: |
yarn build:ios

build-ios-spm:
# SPM path (default, dynamic frameworks).
runs-on: macos-latest
needs: build-library
env:
USE_FRAMEWORKS: dynamic
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Setup
uses: ./.github/actions/setup

- name: Install pods (SPM)
working-directory: ./example/ios
run: pod install

- name: Build example for iOS (SPM)
working-directory: ./example
run: |
yarn build:ios
17 changes: 12 additions & 5 deletions example/ios/Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ require Pod::Executable.execute_command('node', ['-p',
platform :ios, min_ios_version_supported
prepare_react_native_project!

linkage = ENV['USE_FRAMEWORKS']
if linkage != nil
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
use_frameworks! :linkage => linkage.to_sym
end
# freerasp-react-native delivers TalsecRuntime via Swift Package Manager, which
# requires dynamically linked frameworks. Default to dynamic; USE_FRAMEWORKS may
# override the linkage.
linkage = (ENV['USE_FRAMEWORKS'] || 'dynamic').to_sym
Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
use_frameworks! :linkage => linkage

target 'FreeraspRNExample' do
config = use_native_modules!
Expand All @@ -31,5 +32,11 @@ target 'FreeraspRNExample' do
:mac_catalyst_enabled => false,
# :ccache_enabled => true
)

# freeRASP: embed TalsecRuntime into the app target on the SPM path (the helper is a
# no-op / cleanup on the vendored fallback, so it is safe to call unconditionally).
# Real apps resolve the helper from node_modules; this example requires it from the repo root.
require_relative '../../freerasp_spm.rb'
freerasp_embed_talsec_spm!(installer)
end
end
43 changes: 35 additions & 8 deletions freerasp-react-native.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'

# TalsecRuntime: SPM by default (RN >= 0.75), vendored xcframework fallback.
# Opt out of SPM with FREERASP_DISABLE_SPM=1. Pin the EXACT TalsecRuntime version this
# library release is built against (each RN version maps to a specific TalsecRuntime).
talsec_spm_url = 'https://github.com/talsec/Free-RASP-ReactNative-SPM'
talsec_spm_version = '6.14.4'

Pod::Spec.new do |s|
s.name = "freerasp-react-native"
s.version = package["version"]
Expand All @@ -14,14 +20,35 @@ Pod::Spec.new do |s|
s.platforms = { :ios => "11.0" }
s.source = { :git => "https://github.com/talsec/freerasp-react-native.git", :tag => "#{s.version}" }

s.source_files = 'ios/models/*.{h,m,mm,swift}',
'ios/utils/*.{h,m,mm,swift}',
'ios/dispatchers/*.{h,m,mm,swift}',
'ios/*.{h,m,mm,swift}',
'ios/TalsecRuntime.xcframework'
s.xcconfig = { 'OTHER_LDFLAGS' => '-framework TalsecRuntime' }
s.ios.vendored_frameworks = "ios/TalsecRuntime.xcframework"

# SPM is the default whenever the spm_dependency helper is available (RN >= 0.75),
# unless the consumer opts out with FREERASP_DISABLE_SPM=1.
use_spm = respond_to?(:spm_dependency, true) && ENV['FREERASP_DISABLE_SPM'] != '1'

source_globs = [
'ios/models/*.{h,m,mm,swift}',
'ios/utils/*.{h,m,mm,swift}',
'ios/dispatchers/*.{h,m,mm,swift}',
'ios/*.{h,m,mm,swift}',
]

if use_spm
# SPM injects the reference into the Pods project; the framework is embedded into the
# app target by freerasp_embed_talsec_spm! (freerasp_spm.rb), called from the Podfile.
s.ios.deployment_target = '13.0'
spm_dependency(s,
url: talsec_spm_url,
requirement: { kind: 'exactVersion', version: talsec_spm_version },
products: ['TalsecRuntime']
)
else
# Vendored xcframework fallback (RN < 0.75 or FREERASP_DISABLE_SPM=1).
source_globs << 'ios/TalsecRuntime.xcframework'
s.xcconfig = { 'OTHER_LDFLAGS' => '-framework TalsecRuntime' }
s.ios.vendored_frameworks = 'ios/TalsecRuntime.xcframework'
end

s.source_files = source_globs

# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
if respond_to?(:install_modules_dependencies, true)
Expand Down
53 changes: 53 additions & 0 deletions freerasp_spm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# freeRASP — embeds the SPM-delivered TalsecRuntime into the app target(s). spm_dependency
# only attaches it to the pod target, so dyld fails at launch otherwise. Idempotent and safe
# to call unconditionally: removes any stale reference, re-adds only when SPM is active.
# Keep url/requirement in sync with freerasp-react-native.podspec.

def freerasp_embed_talsec_spm!(installer,
url: 'https://github.com/talsec/Free-RASP-ReactNative-SPM',
requirement: { kind: 'exactVersion', version: '6.14.4' },
product: 'TalsecRuntime')

pkg_class = Xcodeproj::Project::Object::XCRemoteSwiftPackageReference
ref_class = Xcodeproj::Project::Object::XCSwiftPackageProductDependency

# Mirror the podspec: SPM active unless unavailable (RN < 0.75) or opted out.
spm_active = respond_to?(:spm_dependency, true) && ENV['FREERASP_DISABLE_SPM'] != '1'

installer.aggregate_targets.each do |aggregate_target|
project = aggregate_target.user_project
next if project.nil?

app_targets = aggregate_target.user_targets.select do |t|
t.respond_to?(:product_type) && t.product_type == 'com.apple.product-type.application'
end

# Remove any previously-added reference first (avoids a double embed on delivery switch).
app_targets.each do |target|
target.package_product_dependencies.delete_if do |r|
r.class == ref_class && r.product_name == product &&
r.package.respond_to?(:repositoryURL) && r.package.repositoryURL == url
end
end
project.root_object.package_references.delete_if do |p|
p.class == pkg_class && p.repositoryURL == url
end

if spm_active
pkg = project.new(pkg_class)
pkg.repositoryURL = url
pkg.requirement = requirement
project.root_object.package_references << pkg

app_targets.each do |target|
Pod::UI.puts "[freeRASP][SPM] Embedding #{product} into app target #{target.name}"
dep = project.new(ref_class)
dep.package = pkg
dep.product_name = product
target.package_product_dependencies << dep
end
end

project.save
end
end
1 change: 1 addition & 0 deletions plugin/build/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { type ConfigPlugin } from '@expo/config-plugins';
import { type PluginConfigType } from './pluginConfig';
declare const _default: ConfigPlugin<PluginConfigType>;
export default _default;
58 changes: 58 additions & 0 deletions plugin/build/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const config_plugins_1 = require("@expo/config-plugins");
const fs_1 = require("fs");
const path_1 = require("path");
const { createBuildGradlePropsConfigPlugin } = config_plugins_1.AndroidConfig.BuildProperties;
const urlFreerasp = 'https://europe-west3-maven.pkg.dev/talsec-artifact-repository/freerasp';
const urlJitpack = 'https://www.jitpack.io';
Expand Down Expand Up @@ -71,10 +73,66 @@ const withAndroidR8Version = (expoConfig, props) => {
return config;
});
};
// iOS — SPM delivery of TalsecRuntime (default on RN >= 0.75). Sets dynamic frameworks
// and injects a guarded TalsecRuntime embed into the Podfile post_install (skipped on the
// vendored fallback / FREERASP_DISABLE_SPM=1). Note: the Expo prebuild flow is less
// battle-tested than bare React Native.
const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)';
/**
* Force dynamically linked frameworks — required by `spm_dependency`.
*/
const withFreeraspIosDynamicFrameworks = (config) => {
return (0, config_plugins_1.withPodfileProperties)(config, (cfg) => {
cfg.modResults['ios.useFrameworks'] = 'dynamic';
return cfg;
});
};
/**
* Inject the TalsecRuntime embed step into the generated Podfile `post_install`,
* so the SPM binary framework ends up in the app bundle (otherwise dyld fails at launch).
*/
const withFreeraspIosSpmEmbed = (config) => {
return (0, config_plugins_1.withDangerousMod)(config, [
'ios',
(cfg) => {
const podfilePath = (0, path_1.join)(cfg.modRequest.platformProjectRoot, 'Podfile');
let contents = (0, fs_1.readFileSync)(podfilePath, 'utf-8');
if (!contents.includes(FREERASP_SPM_EMBED_TAG)) {
const anchor = 'post_install do |installer|';
const anchorIndex = contents.indexOf(anchor);
if (anchorIndex === -1) {
config_plugins_1.WarningAggregator.addWarningIOS('freerasp-react-native', 'Could not find a `post_install` block in the Podfile to inject the ' +
'TalsecRuntime SPM embed step.');
}
else {
const snippet = [
'',
` ${FREERASP_SPM_EMBED_TAG}`,
" require Pod::Executable.execute_command('node', ['-p',",
` 'require.resolve("freerasp-react-native/freerasp_spm.rb", {paths: [process.argv[1]]})',`,
' __dir__]).strip',
' freerasp_embed_talsec_spm!(installer)',
].join('\n');
const insertAt = anchorIndex + anchor.length;
contents =
contents.slice(0, insertAt) + snippet + contents.slice(insertAt);
(0, fs_1.writeFileSync)(podfilePath, contents);
}
}
return cfg;
},
]);
};
const withRnTalsecIos = (config) => {
config = withFreeraspIosDynamicFrameworks(config);
config = withFreeraspIosSpmEmbed(config);
return config;
};
const withRnTalsecApp = (config, props) => {
config = withBuildscriptDependency(config);
config = withAndroidMinSdkVersion(config, props);
config = withAndroidR8Version(config, props);
config = withRnTalsecIos(config);
return config;
};
let pkg = {
Expand Down
68 changes: 68 additions & 0 deletions plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import {
AndroidConfig,
WarningAggregator,
createRunOncePlugin,
withDangerousMod,
withPodfileProperties,
withProjectBuildGradle,
type ConfigPlugin,
} from '@expo/config-plugins';
import { type ExpoConfig } from '@expo/config-types';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { type PluginConfigType } from './pluginConfig';

const { createBuildGradlePropsConfigPlugin } = AndroidConfig.BuildProperties;
Expand Down Expand Up @@ -106,10 +110,74 @@ const withAndroidR8Version: ConfigPlugin<PluginConfigType> = (
});
};

// iOS — SPM delivery of TalsecRuntime (default on RN >= 0.75). Sets dynamic frameworks
// and injects a guarded TalsecRuntime embed into the Podfile post_install (skipped on the
// vendored fallback / FREERASP_DISABLE_SPM=1). Note: the Expo prebuild flow is less
// battle-tested than bare React Native.

const FREERASP_SPM_EMBED_TAG = '# @generated freerasp-react-native (SPM embed)';

/**
* Force dynamically linked frameworks — required by `spm_dependency`.
*/
const withFreeraspIosDynamicFrameworks: ConfigPlugin = (config) => {
return withPodfileProperties(config, (cfg) => {
cfg.modResults['ios.useFrameworks'] = 'dynamic';
return cfg;
});
};

/**
* Inject the TalsecRuntime embed step into the generated Podfile `post_install`,
* so the SPM binary framework ends up in the app bundle (otherwise dyld fails at launch).
*/
const withFreeraspIosSpmEmbed: ConfigPlugin = (config) => {
return withDangerousMod(config, [
'ios',
(cfg) => {
const podfilePath = join(cfg.modRequest.platformProjectRoot, 'Podfile');
let contents = readFileSync(podfilePath, 'utf-8');

if (!contents.includes(FREERASP_SPM_EMBED_TAG)) {
const anchor = 'post_install do |installer|';
const anchorIndex = contents.indexOf(anchor);
if (anchorIndex === -1) {
WarningAggregator.addWarningIOS(
'freerasp-react-native',
'Could not find a `post_install` block in the Podfile to inject the ' +
'TalsecRuntime SPM embed step.'
);
} else {
const snippet = [
'',
` ${FREERASP_SPM_EMBED_TAG}`,
" require Pod::Executable.execute_command('node', ['-p',",
` 'require.resolve("freerasp-react-native/freerasp_spm.rb", {paths: [process.argv[1]]})',`,
' __dir__]).strip',
' freerasp_embed_talsec_spm!(installer)',
].join('\n');
const insertAt = anchorIndex + anchor.length;
contents =
contents.slice(0, insertAt) + snippet + contents.slice(insertAt);
writeFileSync(podfilePath, contents);
}
}
return cfg;
},
]);
};

const withRnTalsecIos: ConfigPlugin = (config) => {
config = withFreeraspIosDynamicFrameworks(config);
config = withFreeraspIosSpmEmbed(config);
return config;
};

const withRnTalsecApp: ConfigPlugin<PluginConfigType> = (config, props) => {
config = withBuildscriptDependency(config);
config = withAndroidMinSdkVersion(config, props);
config = withAndroidR8Version(config, props);
config = withRnTalsecIos(config);
return config;
};

Expand Down
Loading