Skip to content
Draft
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
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,11 @@ js/packages/
`.` (shared host types), `/web` (iframe + Web
Worker), `/worker-runtime` (Worker entry).
WASM bundle (gitignored) under dist/wasm/web/, built via `make wasm`
js/container/ TS lockdown container for the iOS host web view; `npm run build`
bundles it into ios/truapi-host/Sources/TrUAPIHost/Resources/
js/container/ TS lockdown container for the iOS/Android host web views;
per-platform entry points (index-ios/index-android) define
isolation policy (iOS gates network in JS; Android leaves it to a
native interceptor). `npm run build` bundles both: iOS →
ios/truapi-host/.../Resources/, Android → android/truapi-host/.../assets/
ios/truapi-host/ TrUAPIHost Swift package over the truapi-server UniFFI core;
SPM manifest at the repo root (Package.swift), rebuild via
ios/truapi-host/scripts/rebuild.sh
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ js/packages/
truapi-host/ @parity/truapi-host: WASM-backed host runtime; entries `.`
(shared host types), `/web` (iframe + Web Worker),
`/worker-runtime`
js/container/ TS lockdown container for the iOS host web view; bundles into
ios/truapi-host/Sources/TrUAPIHost/Resources/truapi-container.js
js/container/ TS lockdown container for the iOS/Android host web views;
per-platform entry points (index-ios / index-android) bundle to
ios/truapi-host/.../Resources/ and android/truapi-host/.../assets/
(isolation policy + WebRTC gate + bridge contract: js/container/README.md)
android/truapi-host/ Kotlin host adapter package over the truapi-server UniFFI core
ios/truapi-host/ Swift host adapter package over the truapi-server UniFFI core
playground/ Interactive Next.js playground (truapi-playground dotNS label)
Expand Down
285 changes: 231 additions & 54 deletions ios/truapi-host/Sources/TrUAPIHost/Resources/truapi-container.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use strict";
(() => {
// src/index.ts
// src/freeze.ts
function freezeAndDelete(obj, prop) {
try {
Object.defineProperty(obj, prop, {
Expand All @@ -27,72 +27,249 @@
} catch {
}
}
var _nativeFetch = window.fetch.bind(window);
var _NativeWebSocket = window.WebSocket;
var _bridgeUrl = window.__truapi_localhost?.url;
var _GatedWebSocket = new Proxy(window.WebSocket, {
construct(target, args) {
if (_bridgeUrl !== void 0 && args[0] === _bridgeUrl) {
return new _NativeWebSocket(args[0]);

// src/native-transport.ts
var getRandomValues = crypto.getRandomValues.bind(crypto);
var TypedArray = Uint8Array;
var objectCreate = Object.create;
var stringify = JSON.stringify;
var parse = JSON.parse;
var HEX = "0123456789abcdef";
function randomId() {
Comment thread
ERussel marked this conversation as resolved.
const bytes = new TypedArray(16);
getRandomValues(bytes);
let hex = "";
for (let i = 0; i < 16; i++) {
const b = bytes[i];
hex += HEX[b >> 4] + HEX[b & 15];
}
return hex;
}
function createNativeTransport(sendToNative) {
const pending = /* @__PURE__ */ new Map();
function callNative(method, params) {
const id = randomId();
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
const envelope = objectCreate(null);
envelope.type = "request";
envelope.id = id;
envelope.method = method;
envelope.params = params;
sendToNative(stringify(envelope));
});
}
function dispatch(id, payload) {
const entry = pending.get(id);
if (entry === void 0) {
return;
}
throw new TypeError("Network access is not allowed");
pending.delete(id);
let reply;
try {
reply = typeof payload === "string" ? parse(payload) : payload;
} catch {
entry.reject(new Error("Malformed native reply"));
return;
}
if (reply.error !== void 0 && reply.error !== null) {
const error = reply.error;
const code = typeof error === "string" ? error : error.code ?? "native_error";
const message = typeof error === "string" ? error : error.message ?? code;
entry.reject(Object.assign(new Error(message), { code }));
return;
}
entry.resolve(reply.value);
}
});
freezeValue(window, "WebSocket", _GatedWebSocket);
try {
Object.defineProperty(_NativeWebSocket.prototype, "constructor", {
value: _GatedWebSocket,
writable: false,
configurable: false
return { callNative, dispatch };
}

// src/bridge-contract.ts
var HANDLER_NAME = "__container__";
var CALLBACK_NAME = "__container_callback__";
function installNativeBridge(send) {
const transport = createNativeTransport(send);
freezeValue(window, CALLBACK_NAME, (id, payload) => {
transport.dispatch(id, payload);
});
} catch {
return transport;
}
freezeValue(window, "fetch", (input, init) => {
try {
const raw = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
const url = new URL(raw, window.location.href);
if (url.origin === window.location.origin) {
return _nativeFetch(input, init);
}
} catch {

// src/ios-bridge.ts
function getMessageHandler() {
const webkit = window.webkit;
return webkit?.messageHandlers?.[HANDLER_NAME];
}
function createIOSBridge() {
const handler = getMessageHandler();
if (handler === void 0) {
return void 0;
}
return Promise.reject(new TypeError("Network access is not allowed"));
});
freezeAndDelete(window, "XMLHttpRequest");
freezeAndDelete(window, "EventSource");
freezeValue(navigator, "sendBeacon", () => false);
freezeAndDelete(window, "indexedDB");
freezeAndDelete(window, "caches");
try {
Object.defineProperty(document, "cookie", {
get: () => "",
set: () => {
},
configurable: false
const postMessage = handler.postMessage.bind(handler);
return installNativeBridge((message) => {
postMessage(message);
});
} catch {
}
freezeAndDelete(window, "SharedWorker");
if (navigator.serviceWorker) {
try {
Object.defineProperty(navigator, "serviceWorker", {
value: Object.freeze({
register: () => {
throw new Error("ServiceWorker is not available");

// src/webrtc-manager.ts
function createWebRtcAccessRequester(transport) {
return () => transport.callNative("allowWebRtcAccess", {}).then(
(reply) => typeof reply === "object" && reply !== null && reply.allowed === true
);
}
var GATED_METHODS = [
"createOffer",
"createAnswer",
"setLocalDescription",
"setRemoteDescription",
"addIceCandidate"
];
var WebRtcManager = class {
constructor(nativeConnectionClass, requestAccess) {
const decisions = /* @__PURE__ */ new WeakMap();
const ensureAllowed = (connection) => {
let decision = decisions.get(connection);
if (decision === void 0) {
decision = requestAccess();
decisions.set(connection, decision);
}
return decision.then((allowed) => {
if (!allowed) {
connection.close();
throw new TypeError("WebRTC access is not allowed");
}
});
};
const proto = nativeConnectionClass.prototype;
for (const name of GATED_METHODS) {
const nativeMethod = proto[name];
Object.defineProperty(proto, name, {
configurable: false,
writable: false,
enumerable: true,
value: async function(...args) {
await ensureAllowed(this);
return nativeMethod.apply(this, args);
}
}),
});
}
this.connectionClass = nativeConnectionClass;
}
};

// src/lockdown.ts
function gateWebSocketToBridge() {
const NativeWebSocket = window.WebSocket;
const bridgeUrl = window.__truapi_localhost?.url;
const gated = new Proxy(NativeWebSocket, {
construct(_target, args) {
if (bridgeUrl !== void 0 && args[0] === bridgeUrl) {
return new NativeWebSocket(args[0]);
}
throw new TypeError("Network access is not allowed");
}
});
freezeValue(window, "WebSocket", gated);
try {
Object.defineProperty(NativeWebSocket.prototype, "constructor", {
value: gated,
writable: false,
configurable: false
});
} catch {
}
}
var _createElement = document.createElement.bind(document);
freezeValue(document, "createElement", (tagName, options) => {
if (tagName.toLowerCase() === "iframe") {
throw new Error("iframe creation is not allowed");
function gateFetchSameOrigin() {
const nativeFetch = window.fetch.bind(window);
freezeValue(
window,
"fetch",
(input, init) => {
try {
const raw = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
const url = new URL(raw, window.location.href);
if (url.origin === window.location.origin) {
return nativeFetch(input, init);
}
} catch {
}
return Promise.reject(new TypeError("Network access is not allowed"));
}
);
}
function deleteLegacyNetwork() {
freezeAndDelete(window, "XMLHttpRequest");
freezeAndDelete(window, "EventSource");
}
function disableSendBeacon() {
freezeValue(navigator, "sendBeacon", () => false);
}
function gateStorage() {
freezeAndDelete(window, "indexedDB");
freezeAndDelete(window, "caches");
}
function disableCookies() {
try {
Object.defineProperty(document, "cookie", {
get: () => "",
set: () => {
},
configurable: false
});
} catch {
}
return _createElement(tagName, options);
});
freezeAndDelete(window, "RTCPeerConnection");
}
function gateWorkers() {
freezeAndDelete(window, "SharedWorker");
if (navigator.serviceWorker) {
try {
Object.defineProperty(navigator, "serviceWorker", {
value: Object.freeze({
register: () => {
throw new Error("ServiceWorker is not available");
}
}),
writable: false,
configurable: false
});
} catch {
}
}
}
function blockIframeCreation() {
const createElement = document.createElement.bind(document);
freezeValue(
document,
"createElement",
(tagName, options) => {
if (tagName.toLowerCase() === "iframe") {
throw new Error("iframe creation is not allowed");
}
return createElement(tagName, options);
}
);
}
function installWebRtcGate(bridge) {
const NativeRTC = window.RTCPeerConnection;
if (NativeRTC && bridge) {
freezeValue(
window,
"RTCPeerConnection",
new WebRtcManager(NativeRTC, createWebRtcAccessRequester(bridge)).connectionClass
);
} else {
freezeAndDelete(window, "RTCPeerConnection");
}
}

// src/index-ios.ts
gateWebSocketToBridge();
gateFetchSameOrigin();
deleteLegacyNetwork();
disableSendBeacon();
gateStorage();
disableCookies();
gateWorkers();
blockIframeCreation();
installWebRtcGate(createIOSBridge());
})();
2 changes: 1 addition & 1 deletion ios/truapi-host/scripts/rebuild.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ sh "$PACKAGE_ROOT/scripts/sync-bindings.sh"
sh "$PACKAGE_ROOT/scripts/stage-xcframework.sh"

npm --prefix "$TRUAPI_ROOT/js/container" install --no-fund --no-audit
npm --prefix "$TRUAPI_ROOT/js/container" run build
npm --prefix "$TRUAPI_ROOT/js/container" run build:ios

echo "TrUAPIHost package outputs rebuilt in $PACKAGE_ROOT"
Loading