Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
55 changes: 46 additions & 9 deletions js/packages/core/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { NativePayloadResult } from "./lib/wasm";
import { WasmModule, initIDKit } from "./lib/wasm";
import {
isInWorldApp,
getWorldAppVerifyVersion,
createNativeRequest,
type BuilderConfig,
} from "./transports/native";
Expand Down Expand Up @@ -407,19 +408,31 @@ class IDKitBuilder {
*/
async constraints(constraints: ConstraintNode): Promise<IDKitRequest> {
await initIDKit();
const wasmBuilder = createWasmBuilderFromConfig(this.config);

if (isInWorldApp()) {
const verifyVersion = getWorldAppVerifyVersion();

if (verifyVersion < 2) {
// Constraints require v2 — they can't be represented as v1 payloads.
throw new Error(
"verify v2 is not supported by this World App version. " +
"Use a legacy preset (e.g. orbLegacy()) or update the World App.",
);
}

const wasmBuilder = createWasmBuilderFromConfig(this.config);
const wasmResult: NativePayloadResult =
wasmBuilder.nativePayload(constraints);
return createNativeRequest(
wasmResult.payload,
this.config,
wasmResult.signal_hashes ?? {},
2,
);
}

// Bridge path — WASM
const wasmBuilder = createWasmBuilderFromConfig(this.config);
const wasmRequest = (await wasmBuilder.constraints(
constraints,
)) as unknown as WasmModule.IDKitRequest;
Expand All @@ -443,19 +456,43 @@ class IDKitBuilder {
*/
async preset(preset: Preset): Promise<IDKitRequest> {
await initIDKit();
const wasmBuilder = createWasmBuilderFromConfig(this.config);

if (isInWorldApp()) {
const wasmResult: NativePayloadResult =
wasmBuilder.nativePayloadFromPreset(preset);
return createNativeRequest(
wasmResult.payload,
this.config,
wasmResult.signal_hashes ?? {},
);
const verifyVersion = getWorldAppVerifyVersion();

if (verifyVersion === 2) {
const wasmBuilder = createWasmBuilderFromConfig(this.config);
const wasmResult: NativePayloadResult =
wasmBuilder.nativePayloadFromPreset(preset);
return createNativeRequest(
wasmResult.payload,
this.config,
wasmResult.signal_hashes ?? {},
2,
);
}

// v1 — presets always have valid legacy fields, so this should succeed
try {
const wasmBuilder = createWasmBuilderFromConfig(this.config);
const wasmResult: NativePayloadResult =
wasmBuilder.nativePayloadV1FromPreset(preset);
return createNativeRequest(
wasmResult.payload,
this.config,
wasmResult.signal_hashes ?? {},
1,
);
} catch {
throw new Error(
"verify v2 is not supported by this World App version. " +
"Use a legacy preset (e.g. orbLegacy()) or update the World App.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Re-throw non-version errors in v1 preset fallback

The broad catch in the v1 preset branch rewrites every failure into a "verify v2 is not supported" message, including unrelated issues like malformed RP context or other payload-construction errors thrown inside the try block. On hosts that only support v1, this misdiagnoses real configuration problems and points developers to the wrong fix instead of surfacing the actual error.

Useful? React with 👍 / 👎.

);
}
}

// Bridge path — WASM
const wasmBuilder = createWasmBuilderFromConfig(this.config);
const wasmRequest = (await wasmBuilder.preset(
preset,
)) as unknown as WasmModule.IDKitRequest;
Expand Down
64 changes: 63 additions & 1 deletion js/packages/core/src/transports/native.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { IDKitErrorCodes } from "../types/result";
import type { BuilderConfig } from "./native";
import { createNativeRequest } from "./native";
import { createNativeRequest, getWorldAppVerifyVersion } from "./native";
import { hashSignal } from "../lib/hashing";

const baseConfig: BuilderConfig = {
Expand Down Expand Up @@ -194,4 +194,66 @@ describe("native transport request lifecycle", () => {
expect(req2).not.toBe(req1);
activeRequest = req2;
});

it("sends version in postMessage envelope", () => {
const req = createNativeRequest({ data: "test" }, baseConfig, {}, 1);
activeRequest = req;

const postMessageFn = (globalThis as any).window.Android.postMessage;
expect(postMessageFn).toHaveBeenCalledTimes(1);

const sent = JSON.parse(postMessageFn.mock.calls[0][0]);
expect(sent.version).toBe(1);
expect(sent.command).toBe("verify");
});
});

describe("getWorldAppVerifyVersion", () => {
afterEach(() => {
delete (globalThis as any).window;
});

it("returns 2 when verify v2 is listed in supported_commands", () => {
(globalThis as any).window = {
WorldApp: {
supported_commands: [{ name: "verify", supported_versions: [1, 2] }],
},
};

expect(getWorldAppVerifyVersion()).toBe(2);
});

it("returns 1 when verify only supports v1", () => {
(globalThis as any).window = {
WorldApp: {
supported_commands: [{ name: "verify", supported_versions: [1] }],
},
};

expect(getWorldAppVerifyVersion()).toBe(1);
});

it("returns 1 when WorldApp is missing", () => {
(globalThis as any).window = {};

expect(getWorldAppVerifyVersion()).toBe(1);
});

it("returns 1 when supported_commands is missing", () => {
(globalThis as any).window = {
WorldApp: {},
};

expect(getWorldAppVerifyVersion()).toBe(1);
});

it("returns 1 when supported_versions is missing on verify", () => {
(globalThis as any).window = {
WorldApp: {
supported_commands: [{ name: "verify" }],
},
};

expect(getWorldAppVerifyVersion()).toBe(1);
});
});
26 changes: 24 additions & 2 deletions js/packages/core/src/transports/native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,20 @@ export function isInWorldApp(): boolean {
return typeof window !== "undefined" && Boolean((window as any).WorldApp);
}

/**
* Detects the highest verify command version supported by the host World App.
*
* Reads `window.WorldApp.supported_commands` and looks for the "verify" entry.
* Returns `2` when the host explicitly lists version 2; defaults to `1`
* otherwise (safest for older Android builds that reject unknown versions).
*/
export function getWorldAppVerifyVersion(): 1 | 2 {
const cmds = (window as any).WorldApp?.supported_commands;
if (!Array.isArray(cmds)) return 1;
const verify = cmds.find((c: any) => c.name === "verify");
return verify?.supported_versions?.includes(2) ? 2 : 1;
}

// ─────────────────────────────────────────────────────────────────────────────
// Builder config types (shared with request.ts)
// ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -84,19 +98,26 @@ let _activeNativeRequest: NativeIDKitRequest | null = null;
* @param wasmPayload - Pre-built payload from the WASM module (same format as bridge)
* @param config - Builder config (used for response normalization)
* @param signalHashes - Pre-computed signal hashes keyed by identifier (credential type)
* @param version - Verify command version to send in the postMessage envelope (1 or 2, default 2)
*/
export function createNativeRequest(
wasmPayload: unknown,
config: BuilderConfig,
signalHashes: Record<string, string> = {},
version: 1 | 2,
Comment on lines 103 to +107

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Default verify version when creating native requests

createNativeRequest now requires a version argument but does not provide a runtime default, so any existing JavaScript call site still using the old 3-argument form will send { command: "verify", version: undefined } to World App. Because this helper previously hardcoded v2, that is a behavioral regression for untyped/non-typechecked callers and can cause verification requests to be rejected; setting a default (as the docstring already implies) preserves backward compatibility.

Useful? React with 👍 / 👎.

): IDKitRequest {
if (_activeNativeRequest?.isPending()) {
console.warn(
"IDKit native request already in flight. Reusing active request.",
);
return _activeNativeRequest;
}
const request = new NativeIDKitRequest(wasmPayload, config, signalHashes);
const request = new NativeIDKitRequest(
wasmPayload,
config,
signalHashes,
version,
);
_activeNativeRequest = request;
return request;
}
Expand All @@ -117,6 +138,7 @@ class NativeIDKitRequest implements IDKitRequest {
wasmPayload: unknown,
config: BuilderConfig,
signalHashes: Record<string, string> = {},
version: 1 | 2,
) {
this.requestId =
crypto.randomUUID?.() ?? `native-${Date.now()}-${++_requestCounter}`;
Expand Down Expand Up @@ -178,7 +200,7 @@ class NativeIDKitRequest implements IDKitRequest {
// Wrap the WASM-built payload in the postMessage envelope
const sendPayload = {
command: "verify",
version: 2,
version,
payload: wasmPayload,
};

Expand Down
49 changes: 49 additions & 0 deletions rust/core/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,55 @@ pub fn build_request_payload(params: &BridgeConnectionParams) -> Result<serde_js
serde_json::to_value(&payload).map_err(Into::into)
}

/// Builds a v1 (MiniKit legacy) native payload from `BridgeConnectionParams`.
///
/// This produces the payload format expected by older World App versions that
/// only support verify command v1:
///
/// ```json
/// {
/// "verification_level": "orb",
/// "action": "my-action",
/// "signal": "0x..hashed..",
/// "timestamp": "1709136000"
/// }
/// ```
///
/// # Errors
///
/// Returns an error if `legacy_verification_level` is `Deprecated` — this means
/// the request uses v4-only constraints and cannot be represented as a v1 payload.
pub fn build_native_v1_payload(params: &BridgeConnectionParams) -> Result<serde_json::Value> {
if params.legacy_verification_level == VerificationLevel::Deprecated {
return Err(Error::InvalidConfiguration(
"Cannot build v1 native payload: legacy_verification_level is Deprecated. \
Use a legacy preset (e.g. orbLegacy()) or update the World App."
.to_string(),
));
}

let action = match &params.kind {
RequestKind::Uniqueness { action } => action.clone(),
_ => {
return Err(Error::InvalidConfiguration(
"v1 native payload only supports uniqueness proofs".to_string(),
))
}
};

let signal_hash =
crate::crypto::hash_signal(&Signal::from_string(params.legacy_signal.clone()));

let payload = serde_json::json!({
"verification_level": params.legacy_verification_level,
"action": action,
"signal": signal_hash,
"timestamp": params.rp_context.created_at.to_string(),
});

Ok(payload)
}

impl BridgeConnection {
/// Creates a new bridge connection
///
Expand Down
50 changes: 50 additions & 0 deletions rust/core/src/wasm_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,48 @@ impl IDKitBuilderWasm {
Ok(result.into())
}

/// Builds a v1 (legacy) native payload from a preset (synchronous, no bridge connection).
///
/// Used by the native transport when the World App only supports verify v1.
/// Only legacy presets produce valid v1 payloads (constraints always have
/// `Deprecated` verification level and will fail).
///
/// # Errors
///
/// Returns an error if the preset is invalid or v1 payload construction fails.
#[wasm_bindgen(js_name = nativePayloadV1FromPreset)]
pub fn native_payload_v1_from_preset(self, preset_json: JsValue) -> Result<JsValue, JsValue> {
let preset: Preset = serde_wasm_bindgen::from_value(preset_json)
.map_err(|e| JsValue::from_str(&format!("Invalid preset: {e}")))?;

let params = self.config.to_params_from_preset(preset)?;

let payload = crate::bridge::build_native_v1_payload(&params)
.map_err(|e| JsValue::from_str(&format!("Failed to build v1 payload: {e}")))?;

let serializer = serde_wasm_bindgen::Serializer::new().serialize_maps_as_objects(true);
let payload_js = payload
.serialize(&serializer)
.map_err(|e| JsValue::from_str(&format!("Serialization failed: {e}")))?;

let signal_hashes_js = params
.signal_hashes
.serialize(&serializer)
.map_err(|e| JsValue::from_str(&format!("Serialization failed: {e}")))?;

let result = js_sys::Object::new();
js_sys::Reflect::set(&result, &JsValue::from_str("payload"), &payload_js)
.map_err(|e| JsValue::from_str(&format!("Failed to set payload: {e:?}")))?;
js_sys::Reflect::set(
&result,
&JsValue::from_str("signal_hashes"),
&signal_hashes_js,
)
.map_err(|e| JsValue::from_str(&format!("Failed to set signal_hashes: {e:?}")))?;

Ok(result.into())
}

/// Creates a `BridgeConnection` with the given constraints
pub fn constraints(self, constraints_json: JsValue) -> js_sys::Promise {
let config = self.config;
Expand Down Expand Up @@ -1235,6 +1277,14 @@ export interface NativePayloadResult {
payload: unknown;
signal_hashes: Record<string, string>;
}

/** V1 native payload sent to older World App versions (verify command v1) */
export interface NativeV1Payload {
verification_level: string;
action: string;
signal: string;
timestamp: string;
}
"#;

// Export session function types
Expand Down
Loading