Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,4 @@
}
return _createElement(tagName, options);
});
freezeAndDelete(window, "RTCPeerConnection");
})();
5 changes: 4 additions & 1 deletion ios/truapi-host/Sources/TrUAPIHost/truapi.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5718,7 +5718,10 @@ public enum RemotePermission: Equatable, Hashable {
*/domains: [String]
)
/**
* WebRTC media access.
* WebRTC access. Advertised and persistable, but host enforcement is not
* yet implemented: the lockdown container leaves `RTCPeerConnection`
* available to products, and camera/microphone capture is gated by the OS
* permission prompts rather than by this permission.
*/
case webRtc
/**
Expand Down
3 changes: 0 additions & 3 deletions js/container/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,4 @@ freezeValue(document, 'createElement', (tagName: string, options?: ElementCreati
return _createElement(tagName, options);
});

// --- WebRTC: no permission path in TrUAPI mode ---
freezeAndDelete(window, 'RTCPeerConnection');

export {};
21 changes: 14 additions & 7 deletions playground/src/lib/auto-test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { runExample, type LogEntry, type RunResult } from "./example-runner";
import { getClientSync } from "@parity/truapi/sandbox";
import type { MethodInfo, ServiceInfo } from "./services";
import { WEBRTC_SERVICE_NAME } from "./webrtc-check";
import type { DiagnosisStatus } from "@/shared/diagnosis";

export const DIAGNOSIS_ID = "__diagnosis__";
Expand All @@ -20,8 +21,16 @@ const SSO_TIMEOUT_MS = 60_000;
// preimage cap, leaving time for the result to cross the iframe boundary.
const LIVE_ALLOCATION_TIMEOUT_MS = 420_000;

// Services skipped wholesale in the diagnosis until hosts wire them up.
const SKIPPED_SERVICES = new Set(["Coin Payment", "Payment"]);
// Services skipped wholesale in the diagnosis, keyed to the reason shown on the
// skipped rows.
const SKIPPED_SERVICES = new Map<string, string>([
["Coin Payment", "Coin Payment service not yet wired up by hosts"],
["Payment", "Payment service not yet wired up by hosts"],
[
WEBRTC_SERVICE_NAME,
"WebRTC needs a live camera/microphone permission grant; run it interactively from the method browser",
],
]);
// Methods that trigger a host permission/signing prompt, so they need the
// longer signing-class timeout to allow for the user to respond.
const LONG_TIMEOUT_METHODS = new Set([
Expand Down Expand Up @@ -62,11 +71,9 @@ async function runOne({
}: RunOneOpts): Promise<void> {
const id = `${serviceName}/${method.name}`;

if (SKIPPED_SERVICES.has(serviceName)) {
onUpdate(id, {
status: "skipped",
output: `${serviceName} service not yet wired up by hosts`,
});
const skipReason = SKIPPED_SERVICES.get(serviceName);
if (skipReason !== undefined) {
onUpdate(id, { status: "skipped", output: skipReason });
return;
}
if (!method.exampleSource) {
Expand Down
11 changes: 7 additions & 4 deletions playground/src/lib/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import type {
ProductExecutionKind,
ServiceInfo,
} from "@parity/truapi/playground/services-types";
import { WEBRTC_SERVICE } from "./webrtc-check";

export type { MethodInfo, ProductExecutionKind, ServiceInfo };
export { servicesForExecution };

export const services: ServiceInfo[] = servicesForExecution(
generatedServices,
"Spa",
);
// Generated SPA-compatible services plus the synthetic WebRTC browser-capability
// method, which is exercised the same way (an example) but is not a wire method.
export const services: ServiceInfo[] = [
...servicesForExecution(generatedServices, "Spa"),
WEBRTC_SERVICE,
];
81 changes: 81 additions & 0 deletions playground/src/lib/webrtc-check.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { ServiceInfo } from "@parity/truapi/playground/services-types";

// A first-class playground method backed by an example that combines TrUAPI
// permission calls with the browser WebRTC APIs. It runs through the same
// example runner as every other method (`truapi` and `assert`/`console` are
// injected; globals like `navigator`/`RTCPeerConnection` are reachable in the
// runner's function scope), so it shows up in the method browser, ⌘K, and the
// diagnosis alike.
//
// The example follows the product model — request every device permission via
// TrUAPI first, then use the capability:
// 1. requestDevicePermission("Camera") and ("Microphone") — device permissions;
// 2. only once granted, getUserMedia + RTCPeerConnection.createOffer.

export const WEBRTC_SERVICE_NAME = "WebRTC";
export const WEBRTC_METHOD_NAME = "peer_connection";

const WEBRTC_EXAMPLE_SOURCE = `// Fail fast: WebRTC media capture requires a secure context, so bail before
// prompting for any permission if the origin isn't secure.
console.log("secure context:", window.isSecureContext);
assert(
window.isSecureContext,
"Not a secure context — WebRTC media capture requires HTTPS or a localhost/loopback origin.",
);

// Permission phase — request the camera and microphone through TrUAPI up front.
const camera = await truapi.permissions.requestDevicePermission("Camera");
assert(camera.isOk(), "camera permission request failed:", camera);
assert(camera.value.granted, "camera permission denied");
console.log("camera granted");

const microphone = await truapi.permissions.requestDevicePermission("Microphone");
assert(microphone.isOk(), "microphone permission request failed:", microphone);
assert(microphone.value.granted, "microphone permission denied");
console.log("microphone granted");

// Capability phase — permissions granted, now access the capability.
assert(
typeof RTCPeerConnection !== "undefined",
"RTCPeerConnection is unavailable — the host has not wired the WebRTC bridge (fail-closed).",
);
assert(
typeof navigator !== "undefined" && !!navigator.mediaDevices?.getUserMedia,
"navigator.mediaDevices.getUserMedia is unavailable in this host.",
);

const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
console.log(
"media captured:",
stream.getTracks().map((t) => t.kind).join(", "),
);

const pc = new RTCPeerConnection();
try {
for (const track of stream.getTracks()) pc.addTrack(track, stream);
const offer = await pc.createOffer();
assert(!!offer.sdp, "createOffer returned an empty SDP");
console.log("offer created:", offer.type, offer.sdp.length + " bytes of SDP");
} finally {
pc.close();
stream.getTracks().forEach((t) => t.stop());
}`;

/** Synthetic WebRTC method — browsable, runnable, and part of the diagnosis. */
export const WEBRTC_SERVICE: ServiceInfo = {
name: WEBRTC_SERVICE_NAME,
methods: [
{
name: WEBRTC_METHOD_NAME,
type: "unary",
description:
"Requests camera + microphone (device permissions) through TrUAPI, " +
"then — once granted — opens an RTCPeerConnection and creates an " +
"offer with the captured media.",
exampleSource: WEBRTC_EXAMPLE_SOURCE,
},
],
};
5 changes: 4 additions & 1 deletion rust/crates/truapi/src/v01/permissions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ pub enum RemotePermission {
/// Domain patterns requested by the product.
domains: Vec<String>,
},
/// WebRTC media access.
/// WebRTC access. Advertised and persistable, but host enforcement is not
/// yet implemented: the lockdown container leaves `RTCPeerConnection`
/// available to products, and camera/microphone capture is gated by the OS
/// permission prompts rather than by this permission.
#[display("WebRTC connections")]
WebRtc,
/// Submitting transactions on behalf of the user via `remote_chain_transaction_broadcast`.
Expand Down