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
2 changes: 2 additions & 0 deletions src/entries/Background/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
websocketProxyUrl: _websocketProxyUrl,
maxSentData: _maxSentData,
maxRecvData: _maxRecvData,
metadata,
} = request.data;
const notaryUrl = _notaryUrl || (await getNotaryApi());
const websocketProxyUrl = _websocketProxyUrl || (await getProxyApi());
Expand All @@ -547,6 +548,7 @@ async function runPluginProver(request: BackgroundAction, now = Date.now()) {
maxSentData,
secretHeaders,
secretResps,
metadata,
});

await setNotaryRequestStatus(id, 'pending');
Expand Down
157 changes: 148 additions & 9 deletions src/entries/SidePanel/SidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
makePlugin,
PluginConfig,
StepConfig,
InputFieldConfig,
} from '../../utils/misc';
import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png';
import logo from '../../assets/img/icon-128.png';
Expand Down Expand Up @@ -225,13 +226,27 @@ function StepContent(
p2p = false,
clientId = '',
parameterValues,
inputs,
} = props;
const [completed, setCompleted] = useState(false);
const [pending, setPending] = useState(false);
const [error, setError] = useState('');
const [notarizationId, setNotarizationId] = useState('');
const [inputValues, setInputValues] = useState<Record<string, string>>({});
const notaryRequest = useRequestHistory(notarizationId);

useEffect(() => {
if (inputs) {
const initialValues: Record<string, string> = {};
inputs.forEach((input) => {
if (input.defaultValue) {
initialValues[input.name] = input.defaultValue;
}
});
setInputValues(initialValues);
}
}, [inputs]);

const getPlugin = useCallback(async () => {
const hex = (await getPluginByUrl(url)) || _hex;
const arrayBuffer = hexToArrayBuffer(hex!);
Expand All @@ -243,16 +258,31 @@ function StepContent(
if (!plugin) return;
if (index > 0 && !lastResponse) return;

// Validate required input fields
if (inputs) {
for (const input of inputs) {
if (
input.required &&
(!inputValues[input.name] || inputValues[input.name].trim() === '')
) {
setError(`${input.label} is required`);
return;
}
}
}

setPending(true);
setError('');

try {
const out = await plugin.call(
action,
index > 0
? JSON.stringify(lastResponse)
: JSON.stringify(parameterValues),
);
let stepData: any;
if (index > 0) {
stepData = lastResponse;
} else {
stepData = { ...parameterValues, ...inputValues };
}

const out = await plugin.call(action, JSON.stringify(stepData));
const val = JSON.parse(out!.string());
if (val && prover) {
setNotarizationId(val);
Expand All @@ -266,7 +296,16 @@ function StepContent(
} finally {
setPending(false);
}
}, [action, index, lastResponse, prover, getPlugin]);
}, [
action,
index,
lastResponse,
prover,
getPlugin,
inputs,
inputValues,
parameterValues,
]);

const onClick = useCallback(() => {
if (
Expand Down Expand Up @@ -311,8 +350,11 @@ function StepContent(
}, []);

useEffect(() => {
processStep();
}, [processStep]);
// only auto-progress if this step does need inputs
if (!inputs || inputs.length === 0) {
processStep();
}
}, [processStep, inputs]);

let btnContent = null;

Expand Down Expand Up @@ -420,8 +462,105 @@ function StepContent(
</div>
</div>
)}
{inputs && inputs.length > 0 && !completed && (
<div className="flex flex-col gap-3 mt-3">
{inputs.map((input) => (
<InputField
key={input.name}
config={input}
value={inputValues[input.name] || ''}
onChange={(value) =>
setInputValues((prev) => ({ ...prev, [input.name]: value }))
}
/>
))}
</div>
)}
{btnContent}
</div>
</div>
);
}

interface InputFieldProps {
config: InputFieldConfig;
value: string;
onChange: (value: string) => void;
disabled?: boolean;
}

function InputField({
config,
value,
onChange,
disabled = false,
}: InputFieldProps): ReactElement {
const { name, label, type, placeholder, required, options } = config;

const baseClasses =
'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent';

const renderInput = () => {
switch (type) {
case 'textarea':
return (
<textarea
id={name}
name={name}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
required={required}
disabled={disabled}
className={classNames(baseClasses, 'resize-y min-h-[80px]')}
rows={3}
/>
);

case 'select':
return (
<select
id={name}
name={name}
value={value}
onChange={(e) => onChange(e.target.value)}
required={required}
disabled={disabled}
className={baseClasses}
>
<option value="">{placeholder || 'Select an option'}</option>
{options?.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
);

default:
return (
<input
type={type}
id={name}
name={name}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
required={required}
disabled={disabled}
className={baseClasses}
/>
);
}
};

return (
<div className="flex flex-col gap-1">
<label htmlFor={name} className="text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
{renderInput()}
</div>
);
}
9 changes: 9 additions & 0 deletions src/pages/ProofViewer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,15 @@ export default function ProofViewer(props?: {
label="Notary Key"
value={props?.notaryKey || request?.verification?.notaryKey}
/>

{request?.metadata &&
Object.entries(request.metadata).map(([key, value]) => (
<MetadataRow
key={`req-${key}`}
label={`Custom: ${key}`}
value={String(value)}
/>
))}
</div>
)}
</div>
Expand Down
36 changes: 35 additions & 1 deletion src/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,11 +188,13 @@ export const makePlugin = async (
} = {
redirect: function (context: CallContext, off: bigint) {
const r = context.read(off);
if (!r) throw new Error('Failed to read context');
const url = r.text();
browser.tabs.update(tab.id, { url });
},
notarize: function (context: CallContext, off: bigint) {
const r = context.read(off);
if (!r) throw new Error('Failed to read context');
const params = JSON.parse(r.text());
const now = Date.now();
const id = charwise.encode(now).toString('hex');
Expand Down Expand Up @@ -338,7 +340,10 @@ export const makePlugin = async (

const pluginConfig: ExtismPluginOptions = {
useWasi: true,
config: injectedConfig,
config: {
...injectedConfig,
tabId: tab.id?.toString() || '',
},
// allowedHosts: approvedRequests.map((r) => urlify(r.url)?.origin),
functions: {
'extism:host/user': funcs,
Expand All @@ -349,12 +354,23 @@ export const makePlugin = async (
return plugin;
};

export type InputFieldConfig = {
name: string; // Unique identifier for the input field
label: string; // Display label for the input
type: 'text' | 'password' | 'email' | 'number' | 'textarea' | 'select'; // Input field type
placeholder?: string; // Optional placeholder text
required?: boolean; // Whether the field is required
defaultValue?: string; // Default value for the field
options?: { value: string; label: string }[]; // Options for select type
};

export type StepConfig = {
title: string; // Text for the step's title
description?: string; // Text for the step's description (optional)
cta: string; // Text for the step's call-to-action button
action: string; // The function name that this step will execute
prover?: boolean; // Boolean indicating if this step outputs a notarization (optional)
inputs?: InputFieldConfig[]; // Input fields for user data collection (optional)
};

export type PluginConfig = {
Expand Down Expand Up @@ -382,6 +398,7 @@ export const getPluginConfig = async (
): Promise<PluginConfig> => {
const plugin = data instanceof ArrayBuffer ? await makePlugin(data) : data;
const out = await plugin.call('config');
if (!out) throw new Error('Plugin config call returned null');
const config: PluginConfig = JSON.parse(out.string());

assert(typeof config.title === 'string' && config.title.length);
Expand Down Expand Up @@ -439,6 +456,23 @@ export const getPluginConfig = async (
assert(typeof step.cta === 'string' && step.cta.length);
assert(typeof step.action === 'string' && step.action.length);
assert(!step.prover || typeof step.prover === 'boolean');

if (step.inputs) {
for (const input of step.inputs) {
assert(typeof input.name === 'string' && input.name.length);
assert(typeof input.label === 'string' && input.label.length);
assert(!input.placeholder || typeof input.placeholder === 'string');
assert(!input.required || typeof input.required === 'boolean');
assert(!input.defaultValue || typeof input.defaultValue === 'string');
if (input.type === 'select') {
assert(Array.isArray(input.options) && input.options.length > 0);
for (const option of input.options!) {
assert(typeof option.value === 'string');
assert(typeof option.label === 'string');
}
}
}
}
}
}

Expand Down
Loading