mirror of
https://github.com/mprojectscode/obsidian-safe-js-plugin.git
synced 2026-07-22 06:55:38 +00:00
update ui
This commit is contained in:
parent
1167694ee8
commit
c564c13d8a
18 changed files with 824 additions and 522 deletions
|
|
@ -110,52 +110,7 @@ export class WorkerExecutionSession {
|
|||
|
||||
cleanupCallbacks.push(
|
||||
worker.onMessage(message => {
|
||||
const parsedMessage = workerToHostMessageSchema.safeParse(message);
|
||||
if (!parsedMessage.success) {
|
||||
settle({
|
||||
status: 'validation-error',
|
||||
codeHash: options.codeHash,
|
||||
message: 'Worker sent an invalid message.',
|
||||
permissions: [...options.grantedPermissions],
|
||||
elapsedMs: this.now() - options.startedAt,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedMessage.data.executionId !== executionId) {
|
||||
settle({
|
||||
status: 'validation-error',
|
||||
codeHash: options.codeHash,
|
||||
message: 'Worker sent a message for an unknown execution.',
|
||||
permissions: [...options.grantedPermissions],
|
||||
elapsedMs: this.now() - options.startedAt,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedMessage.data.type === 'rpc-request') {
|
||||
this.handleRpcRequest(parsedMessage.data, executionId, options, abortController, () => settled, worker.postMessage.bind(worker));
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedMessage.data.ok) {
|
||||
settle({
|
||||
status: 'success',
|
||||
codeHash: options.codeHash,
|
||||
value: parsedMessage.data.value,
|
||||
permissions: [...options.grantedPermissions],
|
||||
elapsedMs: this.now() - options.startedAt,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
settle({
|
||||
status: 'runtime-error',
|
||||
codeHash: options.codeHash,
|
||||
message: parsedMessage.data.error.message,
|
||||
permissions: [...options.grantedPermissions],
|
||||
elapsedMs: this.now() - options.startedAt,
|
||||
});
|
||||
this.handleWorkerMessage(message, executionId, options, abortController, () => settled, worker.postMessage.bind(worker), settle);
|
||||
}),
|
||||
worker.onError(error => {
|
||||
settle({
|
||||
|
|
@ -178,6 +133,51 @@ export class WorkerExecutionSession {
|
|||
});
|
||||
}
|
||||
|
||||
private handleWorkerMessage(
|
||||
message: unknown,
|
||||
executionId: string,
|
||||
options: WorkerExecutionSessionOptions,
|
||||
abortController: AbortController,
|
||||
isSettled: () => boolean,
|
||||
postMessage: (message: HostRpcResponseMessage) => void,
|
||||
settle: (result: SafeJsExecutionResult) => void,
|
||||
): void {
|
||||
const parsedMessage = workerToHostMessageSchema.safeParse(message);
|
||||
if (!parsedMessage.success) {
|
||||
settle(this.createValidationErrorResult(options, 'Worker sent an invalid message.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedMessage.data.executionId !== executionId) {
|
||||
settle(this.createValidationErrorResult(options, 'Worker sent a message for an unknown execution.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedMessage.data.type === 'rpc-request') {
|
||||
this.handleRpcRequest(parsedMessage.data, executionId, options, abortController, isSettled, postMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedMessage.data.ok) {
|
||||
settle({
|
||||
status: 'success',
|
||||
codeHash: options.codeHash,
|
||||
value: parsedMessage.data.value,
|
||||
permissions: [...options.grantedPermissions],
|
||||
elapsedMs: this.now() - options.startedAt,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
settle({
|
||||
status: 'runtime-error',
|
||||
codeHash: options.codeHash,
|
||||
message: parsedMessage.data.error.message,
|
||||
permissions: [...options.grantedPermissions],
|
||||
elapsedMs: this.now() - options.startedAt,
|
||||
});
|
||||
}
|
||||
|
||||
private handleRpcRequest(
|
||||
rpcMessage: HostRpcRequestMessage,
|
||||
executionId: string,
|
||||
|
|
@ -245,4 +245,14 @@ export class WorkerExecutionSession {
|
|||
elapsedMs: this.now() - startedAt,
|
||||
};
|
||||
}
|
||||
|
||||
private createValidationErrorResult(options: WorkerExecutionSessionOptions, message: string): SafeJsExecutionResult {
|
||||
return {
|
||||
status: 'validation-error',
|
||||
codeHash: options.codeHash,
|
||||
message,
|
||||
permissions: [...options.grantedPermissions],
|
||||
elapsedMs: this.now() - options.startedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
89
packages/obsidian/src/rpc/rpc-registry-types.ts
Normal file
89
packages/obsidian/src/rpc/rpc-registry-types.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import type {
|
||||
JsonValue,
|
||||
PermissionDefinition,
|
||||
PermissionId,
|
||||
SafeJsValidationContext,
|
||||
SafeJsValidationResult,
|
||||
SafeJsValidator,
|
||||
SafeJsValidatorReference,
|
||||
} from '@lemons_dev/obsidian-safe-js-api';
|
||||
import type { WorkerRpcBinding } from '@lemons_dev/obsidian-safe-js-api/internal';
|
||||
import type { BuiltInValidatorOptions } from 'packages/obsidian/src/rpc/validators';
|
||||
|
||||
export interface RpcContext {
|
||||
grantedPermissions: ReadonlySet<PermissionId>;
|
||||
codeHash?: string;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface RpcDispatchSuccess {
|
||||
ok: true;
|
||||
result: JsonValue;
|
||||
}
|
||||
|
||||
export interface RpcDispatchFailure {
|
||||
ok: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type RpcDispatchResult = RpcDispatchSuccess | RpcDispatchFailure;
|
||||
|
||||
export interface RpcMethodDefinition<TParams = unknown, TResult = unknown> {
|
||||
method: string;
|
||||
permission: PermissionId;
|
||||
description: string;
|
||||
usage: string;
|
||||
requestValidator: SafeJsValidatorReference<TParams>;
|
||||
responseValidator: SafeJsValidatorReference<TResult>;
|
||||
binding: Omit<WorkerRpcBinding, 'method' | 'permission'>;
|
||||
handler(params: TParams, context: RpcContext): Promise<TResult> | TResult;
|
||||
}
|
||||
|
||||
export interface RpcRegistrationOwner {
|
||||
pluginId?: string;
|
||||
pluginName?: string;
|
||||
}
|
||||
|
||||
export interface SandboxGlobalRegistration {
|
||||
name: string;
|
||||
description: string;
|
||||
value: JsonValue;
|
||||
permission?: PermissionId;
|
||||
}
|
||||
|
||||
export interface RpcDocsMethod {
|
||||
method: string;
|
||||
apiPath: string;
|
||||
description: string;
|
||||
usage: string;
|
||||
permission: PermissionId;
|
||||
ownerPluginId?: string;
|
||||
ownerPluginName?: string;
|
||||
}
|
||||
|
||||
export interface RpcDocsPermission {
|
||||
permission: PermissionDefinition;
|
||||
methods: RpcDocsMethod[];
|
||||
globals: RpcDocsGlobal[];
|
||||
ownerPluginId?: string;
|
||||
ownerPluginName?: string;
|
||||
}
|
||||
|
||||
export interface RpcDocsGlobal {
|
||||
name: string;
|
||||
description: string;
|
||||
permission?: PermissionId;
|
||||
ownerPluginId?: string;
|
||||
ownerPluginName?: string;
|
||||
}
|
||||
|
||||
export interface RpcRegistryOptions {
|
||||
methods?: readonly RpcMethodDefinition[];
|
||||
permissionDefinitions?: readonly PermissionDefinition[];
|
||||
validators: BuiltInValidatorOptions | readonly SafeJsValidator[];
|
||||
}
|
||||
|
||||
export type { JsonValue, PermissionDefinition, PermissionId, SafeJsValidationContext, SafeJsValidationResult, SafeJsValidator, SafeJsValidatorReference };
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import type {
|
||||
JsonValue,
|
||||
PermissionDefinition,
|
||||
PermissionId,
|
||||
SafeJsRegistration,
|
||||
|
|
@ -11,80 +10,33 @@ import type {
|
|||
import type { WorkerRpcBinding, WorkerSandboxGlobal } from '@lemons_dev/obsidian-safe-js-api/internal';
|
||||
import { jsonValueSchema } from 'packages/obsidian/src/execution/contracts';
|
||||
import { PERMISSION_DEFINITIONS, getPermissionDefinition } from 'packages/obsidian/src/permissions/permissions';
|
||||
import type {
|
||||
JsonValue,
|
||||
RpcContext,
|
||||
RpcDispatchResult,
|
||||
RpcDocsPermission,
|
||||
RpcMethodDefinition,
|
||||
RpcRegistrationOwner,
|
||||
RpcRegistryOptions,
|
||||
SandboxGlobalRegistration,
|
||||
} from 'packages/obsidian/src/rpc/rpc-registry-types';
|
||||
import type { BuiltInValidatorOptions } from 'packages/obsidian/src/rpc/validators';
|
||||
import { ValidatorRegistry, createBuiltInValidators } from 'packages/obsidian/src/rpc/validators';
|
||||
|
||||
export type { SafeJsRegistration } from '@lemons_dev/obsidian-safe-js-api';
|
||||
|
||||
export interface RpcContext {
|
||||
grantedPermissions: ReadonlySet<PermissionId>;
|
||||
codeHash?: string;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface RpcDispatchSuccess {
|
||||
ok: true;
|
||||
result: JsonValue;
|
||||
}
|
||||
|
||||
export interface RpcDispatchFailure {
|
||||
ok: false;
|
||||
error: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type RpcDispatchResult = RpcDispatchSuccess | RpcDispatchFailure;
|
||||
|
||||
export interface RpcMethodDefinition<TParams = unknown, TResult = unknown> {
|
||||
method: string;
|
||||
permission: PermissionId;
|
||||
description: string;
|
||||
usage: string;
|
||||
requestValidator: SafeJsValidatorReference<TParams>;
|
||||
responseValidator: SafeJsValidatorReference<TResult>;
|
||||
binding: Omit<WorkerRpcBinding, 'method' | 'permission'>;
|
||||
handler(params: TParams, context: RpcContext): Promise<TResult> | TResult;
|
||||
}
|
||||
|
||||
export interface RpcRegistrationOwner {
|
||||
pluginId?: string;
|
||||
pluginName?: string;
|
||||
}
|
||||
|
||||
export interface SandboxGlobalRegistration {
|
||||
name: string;
|
||||
description: string;
|
||||
value: JsonValue;
|
||||
permission?: PermissionId;
|
||||
}
|
||||
|
||||
export interface RpcDocsMethod {
|
||||
method: string;
|
||||
apiPath: string;
|
||||
description: string;
|
||||
usage: string;
|
||||
permission: PermissionId;
|
||||
ownerPluginId?: string;
|
||||
ownerPluginName?: string;
|
||||
}
|
||||
|
||||
export interface RpcDocsPermission {
|
||||
permission: PermissionDefinition;
|
||||
methods: RpcDocsMethod[];
|
||||
globals: RpcDocsGlobal[];
|
||||
ownerPluginId?: string;
|
||||
ownerPluginName?: string;
|
||||
}
|
||||
|
||||
export interface RpcDocsGlobal {
|
||||
name: string;
|
||||
description: string;
|
||||
permission?: PermissionId;
|
||||
ownerPluginId?: string;
|
||||
ownerPluginName?: string;
|
||||
}
|
||||
export type {
|
||||
RpcContext,
|
||||
RpcDispatchFailure,
|
||||
RpcDispatchResult,
|
||||
RpcDispatchSuccess,
|
||||
RpcDocsGlobal,
|
||||
RpcDocsMethod,
|
||||
RpcDocsPermission,
|
||||
RpcMethodDefinition,
|
||||
RpcRegistrationOwner,
|
||||
RpcRegistryOptions,
|
||||
SandboxGlobalRegistration,
|
||||
} from 'packages/obsidian/src/rpc/rpc-registry-types';
|
||||
|
||||
export class RpcRegistry {
|
||||
private readonly methods = new Map<string, RpcMethodDefinition<unknown, unknown>>();
|
||||
|
|
@ -95,23 +47,19 @@ export class RpcRegistry {
|
|||
private readonly sandboxGlobals = new Map<string, SandboxGlobalRegistration>();
|
||||
private readonly sandboxGlobalOwners = new Map<string, RpcRegistrationOwner>();
|
||||
|
||||
constructor(
|
||||
methods: readonly RpcMethodDefinition[] = [],
|
||||
permissionDefinitions: readonly PermissionDefinition[] = PERMISSION_DEFINITIONS,
|
||||
validatorOptions: BuiltInValidatorOptions | readonly SafeJsValidator[],
|
||||
) {
|
||||
this.validators = new ValidatorRegistry(isValidatorList(validatorOptions) ? validatorOptions : createBuiltInValidators(validatorOptions));
|
||||
constructor(options: RpcRegistryOptions) {
|
||||
this.validators = new ValidatorRegistry(isValidatorList(options.validators) ? options.validators : createBuiltInValidators(options.validators));
|
||||
|
||||
for (const permission of permissionDefinitions) {
|
||||
for (const permission of options.permissionDefinitions ?? PERMISSION_DEFINITIONS) {
|
||||
this.permissionDefinitions.set(permission.id, permission);
|
||||
}
|
||||
|
||||
for (const method of methods) {
|
||||
this.register(method);
|
||||
for (const method of options.methods ?? []) {
|
||||
this.addMethod(method);
|
||||
}
|
||||
}
|
||||
|
||||
register(method: RpcMethodDefinition<unknown, unknown>, owner: RpcRegistrationOwner = {}): void {
|
||||
private addMethod(method: RpcMethodDefinition<unknown, unknown>, owner: RpcRegistrationOwner = {}): void {
|
||||
if (this.methods.has(method.method)) {
|
||||
throw new Error(`Duplicate RPC method '${method.method}'.`);
|
||||
}
|
||||
|
|
@ -162,7 +110,7 @@ export class RpcRegistry {
|
|||
}
|
||||
|
||||
registerMethod(method: RpcMethodDefinition<unknown, unknown>, owner: RpcRegistrationOwner = {}): SafeJsRegistration {
|
||||
this.register(method, owner);
|
||||
this.addMethod(method, owner);
|
||||
|
||||
return {
|
||||
unregister: (): void => {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ import { createWorkspaceMethods } from 'packages/obsidian/src/rpc/obsidian/works
|
|||
import { RpcRegistry } from 'packages/obsidian/src/rpc/rpc-registry';
|
||||
|
||||
export function createSafeJsRpcRegistry(app: App): RpcRegistry {
|
||||
return new RpcRegistry(
|
||||
[
|
||||
return new RpcRegistry({
|
||||
methods: [
|
||||
...createCoreMethods(app),
|
||||
...createHelperMethods(),
|
||||
...createVaultReadMethods(app),
|
||||
|
|
@ -32,7 +32,6 @@ export function createSafeJsRpcRegistry(app: App): RpcRegistry {
|
|||
...createNetworkMethods(),
|
||||
...createStorageMethods(app),
|
||||
],
|
||||
undefined,
|
||||
{ getConfigDir: () => app.vault.configDir },
|
||||
);
|
||||
validators: { getConfigDir: () => app.vault.configDir },
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ function uniqueScriptId(baseId: string, existingIds: readonly string[]): string
|
|||
return `${baseId}-${index}`;
|
||||
}
|
||||
|
||||
function displayNameFromPath(path: string): string {
|
||||
export function displayNameFromPath(path: string): string {
|
||||
const fileName = path.split('/').pop() ?? path;
|
||||
return fileName.replace(/\.js$/iu, '') || 'Script';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,336 +1,3 @@
|
|||
import type { App } from 'obsidian';
|
||||
import { Notice, PluginSettingTab, Setting } from 'obsidian';
|
||||
import type SafeJsPlugin from 'packages/obsidian/src/main';
|
||||
import type { PermissionApproval, PermissionSettingsStore } from 'packages/obsidian/src/permissions/approval-store';
|
||||
import {
|
||||
AppPermissionStorage,
|
||||
LocalStoragePermissionApprovalStore,
|
||||
LocalStoragePermissionSettingsStore,
|
||||
} from 'packages/obsidian/src/permissions/approval-store';
|
||||
import type { SafeJsScriptConfig } from 'packages/obsidian/src/scripts/script-settings';
|
||||
import { createScriptConfig, isJavaScriptVaultScriptPath, normalizeSafeJsScripts } from 'packages/obsidian/src/scripts/script-settings';
|
||||
import type { VaultScriptManager } from 'packages/obsidian/src/scripts/vault-script-manager';
|
||||
import type { ScriptStorageEntry } from 'packages/obsidian/src/storage/script-storage';
|
||||
import { ScriptStorageManager } from 'packages/obsidian/src/storage/script-storage';
|
||||
|
||||
export interface SafeJsSettings {
|
||||
executionTimeoutsEnabled: boolean;
|
||||
executionTimeoutMs: number;
|
||||
debugBlocksEnabled: boolean;
|
||||
scripts: SafeJsScriptConfig[];
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: SafeJsSettings = {
|
||||
executionTimeoutsEnabled: true,
|
||||
executionTimeoutMs: 5000,
|
||||
debugBlocksEnabled: true,
|
||||
scripts: [],
|
||||
};
|
||||
|
||||
export class SafeJsSettingTab extends PluginSettingTab {
|
||||
plugin: SafeJsPlugin;
|
||||
private readonly approvalStore: LocalStoragePermissionApprovalStore;
|
||||
private readonly permissionSettingsStore: PermissionSettingsStore;
|
||||
private readonly scriptManager?: VaultScriptManager;
|
||||
private readonly staleEntryAgeMs = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: SafeJsPlugin,
|
||||
permissionSettingsStore: PermissionSettingsStore = new LocalStoragePermissionSettingsStore(new AppPermissionStorage(app)),
|
||||
scriptManager?: VaultScriptManager,
|
||||
) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
this.approvalStore = new LocalStoragePermissionApprovalStore(new AppPermissionStorage(app));
|
||||
this.permissionSettingsStore = permissionSettingsStore;
|
||||
this.scriptManager = scriptManager;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
|
||||
containerEl.empty();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Execution timeouts')
|
||||
.setDesc('Cancel scripts that run longer than the configured timeout.')
|
||||
.addToggle(toggle =>
|
||||
toggle.setValue(this.plugin.settings.executionTimeoutsEnabled).onChange(async value => {
|
||||
this.plugin.settings.executionTimeoutsEnabled = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Execution timeout')
|
||||
.setDesc(
|
||||
this.plugin.settings.executionTimeoutsEnabled
|
||||
? 'Maximum time a worker-backed script may run before it is cancelled.'
|
||||
: 'Timeouts are disabled. This value is kept for later use.',
|
||||
)
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder(String(DEFAULT_SETTINGS.executionTimeoutMs))
|
||||
.setValue(String(this.plugin.settings.executionTimeoutMs))
|
||||
.onChange(async value => {
|
||||
const parsedValue = Number.parseInt(value, 10);
|
||||
this.plugin.settings.executionTimeoutMs =
|
||||
Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : DEFAULT_SETTINGS.executionTimeoutMs;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Debug blocks')
|
||||
.setDesc('Enable support for the safe-js-debug code block language.')
|
||||
.addToggle(toggle =>
|
||||
toggle.setValue(this.plugin.settings.debugBlocksEnabled).onChange(async value => {
|
||||
this.plugin.settings.debugBlocksEnabled = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-allow low-risk permissions')
|
||||
.setDesc('Allow low-risk permissions without prompting. Approvals are still remembered per script hash on this device.')
|
||||
.addToggle(toggle =>
|
||||
toggle.setValue(this.permissionSettingsStore.loadAutoAllowLowRiskPermissions()).onChange(value => {
|
||||
this.permissionSettingsStore.saveAutoAllowLowRiskPermissions(value);
|
||||
}),
|
||||
);
|
||||
|
||||
this.renderVaultScripts(containerEl);
|
||||
this.renderApprovalStorage(containerEl);
|
||||
this.renderScriptStorage(containerEl);
|
||||
}
|
||||
|
||||
private renderVaultScripts(containerEl: HTMLElement): void {
|
||||
const section = containerEl.createEl('section');
|
||||
new Setting(section).setName('Vault scripts').setHeading();
|
||||
section.createEl('p', {
|
||||
text: `${this.plugin.settings.scripts.length} vault ${this.plugin.settings.scripts.length === 1 ? 'script is' : 'scripts are'} configured.`,
|
||||
});
|
||||
|
||||
for (const script of this.plugin.settings.scripts) {
|
||||
this.renderVaultScript(section, script);
|
||||
}
|
||||
|
||||
this.renderAddVaultScript(section);
|
||||
}
|
||||
|
||||
private renderVaultScript(section: HTMLElement, script: SafeJsScriptConfig): void {
|
||||
new Setting(section)
|
||||
.setName(script.name)
|
||||
.setDesc(script.path)
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder('Script name')
|
||||
.setValue(script.name)
|
||||
.onChange(async value => {
|
||||
script.name = value.trim() || script.path;
|
||||
await this.saveScriptSettings();
|
||||
}),
|
||||
)
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder('Scripts/example.js')
|
||||
.setValue(script.path)
|
||||
.onChange(async value => {
|
||||
script.path = value.trim();
|
||||
await this.saveScriptSettings();
|
||||
}),
|
||||
)
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setTooltip('Run on startup')
|
||||
.setValue(script.runOnStartup)
|
||||
.onChange(async value => {
|
||||
script.runOnStartup = value;
|
||||
await this.saveScriptSettings();
|
||||
}),
|
||||
)
|
||||
.addButton(button =>
|
||||
button.setButtonText('Remove').onClick(async () => {
|
||||
this.plugin.settings.scripts = this.plugin.settings.scripts.filter(candidate => candidate.id !== script.id);
|
||||
await this.saveScriptSettings();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private renderAddVaultScript(section: HTMLElement): void {
|
||||
let path = '';
|
||||
let name = '';
|
||||
|
||||
new Setting(section)
|
||||
.setName('Add script')
|
||||
.setDesc('Configure a vault .js file as a command.')
|
||||
.addText(text =>
|
||||
text.setPlaceholder('Scripts/example.js').onChange(value => {
|
||||
path = value;
|
||||
}),
|
||||
)
|
||||
.addText(text =>
|
||||
text.setPlaceholder('Command name').onChange(value => {
|
||||
name = value;
|
||||
}),
|
||||
)
|
||||
.addButton(button =>
|
||||
button.setButtonText('Add').onClick(async () => {
|
||||
const normalizedPath = path.trim();
|
||||
if (normalizedPath === '' || !isJavaScriptVaultScriptPath(normalizedPath)) {
|
||||
new Notice('Enter a vault path ending in .js.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.plugin.settings.scripts.some(script => script.path === normalizedPath)) {
|
||||
new Notice('That script is already configured.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.plugin.settings.scripts = [...this.plugin.settings.scripts, createScriptConfig(normalizedPath, name, this.plugin.settings.scripts)];
|
||||
await this.saveScriptSettings();
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async saveScriptSettings(): Promise<void> {
|
||||
await this.plugin.saveSettings();
|
||||
this.scriptManager?.reloadCommands();
|
||||
}
|
||||
|
||||
private renderApprovalStorage(containerEl: HTMLElement): void {
|
||||
const approvals = this.approvalStore.list();
|
||||
const section = containerEl.createEl('section');
|
||||
new Setting(section).setName('Approved script hashes').setHeading();
|
||||
section.createEl('p', {
|
||||
text: `${approvals.length} approved script ${approvals.length === 1 ? 'hash' : 'hashes'} are stored on this device.`,
|
||||
});
|
||||
|
||||
new Setting(section)
|
||||
.setName('Clear approved hashes')
|
||||
.setDesc('Remove remembered permission approvals for changed or old scripts.')
|
||||
.addButton(button =>
|
||||
button.setButtonText('Clear older than 30 days').onClick(() => {
|
||||
const deletedCount = this.approvalStore.deleteOlderThan(Date.now() - this.staleEntryAgeMs);
|
||||
new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`);
|
||||
this.display();
|
||||
}),
|
||||
)
|
||||
.addButton(button =>
|
||||
button.setButtonText('Clear all').onClick(() => {
|
||||
const deletedCount = this.approvalStore.deleteAll();
|
||||
new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`);
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
this.renderApprovalList(section, approvals);
|
||||
}
|
||||
|
||||
private renderApprovalList(section: HTMLElement, approvals: PermissionApproval[]): void {
|
||||
if (approvals.length === 0) {
|
||||
section.createEl('p', { text: 'No approved script hashes are stored.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const list = section.createEl('ul');
|
||||
for (const approval of approvals.slice(0, 20)) {
|
||||
const item = list.createEl('li');
|
||||
item.createEl('code', { text: approval.codeHash });
|
||||
item.createSpan({
|
||||
text: ` - ${formatCaller(approval.callerPluginId)} - ${approval.permissions.join(', ')} - ${formatDate(approval.updatedAt)}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (approvals.length > 20) {
|
||||
section.createEl('p', { text: `${approvals.length - 20} more approved hashes are hidden.` });
|
||||
}
|
||||
}
|
||||
|
||||
private renderScriptStorage(containerEl: HTMLElement): void {
|
||||
const entries = ScriptStorageManager.listAll(this.app);
|
||||
const section = containerEl.createEl('section');
|
||||
new Setting(section).setName('Script storage').setHeading();
|
||||
section.createEl('p', {
|
||||
text: `${entries.length} Safe JS storage ${entries.length === 1 ? 'key is' : 'keys are'} indexed on this device.`,
|
||||
});
|
||||
|
||||
new Setting(section)
|
||||
.setName('Clear script storage')
|
||||
.setDesc('Remove data written through the storage API by scripts.')
|
||||
.addButton(button =>
|
||||
button.setButtonText('Clear older than 30 days').onClick(() => {
|
||||
const deletedCount = ScriptStorageManager.deleteOlderThanAll(this.app, Date.now() - this.staleEntryAgeMs);
|
||||
new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`);
|
||||
this.display();
|
||||
}),
|
||||
)
|
||||
.addButton(button =>
|
||||
button.setButtonText('Clear all').onClick(() => {
|
||||
const deletedCount = ScriptStorageManager.deleteAllKnown(this.app);
|
||||
new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`);
|
||||
this.display();
|
||||
}),
|
||||
);
|
||||
|
||||
this.renderScriptStorageList(section, entries);
|
||||
}
|
||||
|
||||
private renderScriptStorageList(section: HTMLElement, entries: ScriptStorageEntry[]): void {
|
||||
if (entries.length === 0) {
|
||||
section.createEl('p', { text: 'No indexed script storage keys are stored.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const list = section.createEl('ul');
|
||||
for (const entry of entries.slice(0, 20)) {
|
||||
const item = list.createEl('li');
|
||||
item.createEl('code', { text: entry.key });
|
||||
item.createSpan({ text: ` - ${formatScope(entry.scope)} - ${formatBytes(entry.sizeBytes)} - ${formatDate(entry.updatedAt)}` });
|
||||
}
|
||||
|
||||
if (entries.length > 20) {
|
||||
section.createEl('p', { text: `${entries.length - 20} more storage keys are hidden.` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
|
||||
function formatCaller(callerPluginId: string | undefined): string {
|
||||
return callerPluginId ?? 'notes';
|
||||
}
|
||||
|
||||
function formatScope(scope: string | null): string {
|
||||
return scope === null ? 'global' : `scoped ${scope.slice(0, 12)}`;
|
||||
}
|
||||
|
||||
export function normalizeSafeJsSettings(value: unknown): SafeJsSettings {
|
||||
const record = typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {};
|
||||
const executionTimeoutMs =
|
||||
typeof record.executionTimeoutMs === 'number' && Number.isFinite(record.executionTimeoutMs) && record.executionTimeoutMs > 0
|
||||
? record.executionTimeoutMs
|
||||
: DEFAULT_SETTINGS.executionTimeoutMs;
|
||||
|
||||
return {
|
||||
executionTimeoutsEnabled:
|
||||
typeof record.executionTimeoutsEnabled === 'boolean' ? record.executionTimeoutsEnabled : DEFAULT_SETTINGS.executionTimeoutsEnabled,
|
||||
executionTimeoutMs,
|
||||
debugBlocksEnabled: typeof record.debugBlocksEnabled === 'boolean' ? record.debugBlocksEnabled : DEFAULT_SETTINGS.debugBlocksEnabled,
|
||||
scripts: normalizeSafeJsScripts(record.scripts),
|
||||
};
|
||||
}
|
||||
export { DEFAULT_SETTINGS, normalizeSafeJsSettings } from 'packages/obsidian/src/settings/settings-schema';
|
||||
export type { SafeJsSettings } from 'packages/obsidian/src/settings/settings-schema';
|
||||
export { SafeJsSettingTab } from 'packages/obsidian/src/ui/settings-tab';
|
||||
|
|
|
|||
32
packages/obsidian/src/settings/settings-schema.ts
Normal file
32
packages/obsidian/src/settings/settings-schema.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { SafeJsScriptConfig } from 'packages/obsidian/src/scripts/script-settings';
|
||||
import { normalizeSafeJsScripts } from 'packages/obsidian/src/scripts/script-settings';
|
||||
|
||||
export interface SafeJsSettings {
|
||||
executionTimeoutsEnabled: boolean;
|
||||
executionTimeoutMs: number;
|
||||
debugBlocksEnabled: boolean;
|
||||
scripts: SafeJsScriptConfig[];
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: SafeJsSettings = {
|
||||
executionTimeoutsEnabled: true,
|
||||
executionTimeoutMs: 5000,
|
||||
debugBlocksEnabled: true,
|
||||
scripts: [],
|
||||
};
|
||||
|
||||
export function normalizeSafeJsSettings(value: unknown): SafeJsSettings {
|
||||
const record = typeof value === 'object' && value !== null ? (value as Record<string, unknown>) : {};
|
||||
const executionTimeoutMs =
|
||||
typeof record.executionTimeoutMs === 'number' && Number.isFinite(record.executionTimeoutMs) && record.executionTimeoutMs > 0
|
||||
? record.executionTimeoutMs
|
||||
: DEFAULT_SETTINGS.executionTimeoutMs;
|
||||
|
||||
return {
|
||||
executionTimeoutsEnabled:
|
||||
typeof record.executionTimeoutsEnabled === 'boolean' ? record.executionTimeoutsEnabled : DEFAULT_SETTINGS.executionTimeoutsEnabled,
|
||||
executionTimeoutMs,
|
||||
debugBlocksEnabled: typeof record.debugBlocksEnabled === 'boolean' ? record.debugBlocksEnabled : DEFAULT_SETTINGS.debugBlocksEnabled,
|
||||
scripts: normalizeSafeJsScripts(record.scripts),
|
||||
};
|
||||
}
|
||||
118
packages/obsidian/src/ui/add-vault-script-modal.ts
Normal file
118
packages/obsidian/src/ui/add-vault-script-modal.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import type { App } from 'obsidian';
|
||||
import type { TextComponent } from 'obsidian';
|
||||
import { Modal, Notice, Setting } from 'obsidian';
|
||||
import { isJavaScriptVaultScriptPath } from 'packages/obsidian/src/scripts/script-settings';
|
||||
|
||||
export interface AddVaultScriptModalValues {
|
||||
name: string;
|
||||
path: string;
|
||||
runOnStartup: boolean;
|
||||
}
|
||||
|
||||
export interface AddVaultScriptModalSubmitResult {
|
||||
message?: string;
|
||||
saved: boolean;
|
||||
}
|
||||
|
||||
export interface AddVaultScriptModalOptions {
|
||||
actionText: string;
|
||||
initialValues: AddVaultScriptModalValues;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export class AddVaultScriptModal extends Modal {
|
||||
private readonly actionText: string;
|
||||
private readonly onSubmit: (values: AddVaultScriptModalValues) => Promise<AddVaultScriptModalSubmitResult>;
|
||||
private readonly title: string;
|
||||
private path: string;
|
||||
private name: string;
|
||||
private runOnStartup: boolean;
|
||||
|
||||
constructor(app: App, options: AddVaultScriptModalOptions, onSubmit: (values: AddVaultScriptModalValues) => Promise<AddVaultScriptModalSubmitResult>) {
|
||||
super(app);
|
||||
this.actionText = options.actionText;
|
||||
this.name = options.initialValues.name;
|
||||
this.onSubmit = onSubmit;
|
||||
this.path = options.initialValues.path;
|
||||
this.runOnStartup = options.initialValues.runOnStartup;
|
||||
this.title = options.title;
|
||||
}
|
||||
|
||||
onOpen(): void {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl('h2', { text: this.title });
|
||||
let pathInput: TextComponent | undefined;
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Vault path')
|
||||
.setDesc('Use a vault-relative path that ends in .js.')
|
||||
.addText(text => {
|
||||
pathInput = text;
|
||||
return text
|
||||
.setPlaceholder('Scripts/example.js')
|
||||
.setValue(this.path)
|
||||
.onChange(value => {
|
||||
this.path = value;
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Command name')
|
||||
.setDesc('Leave blank to use the file name.')
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder('Example script')
|
||||
.setValue(this.name)
|
||||
.onChange(value => {
|
||||
this.name = value;
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.setName('Run on startup')
|
||||
.setDesc('Run this script when Obsidian finishes loading.')
|
||||
.addToggle(toggle =>
|
||||
toggle.setValue(this.runOnStartup).onChange(value => {
|
||||
this.runOnStartup = value;
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(contentEl)
|
||||
.addButton(button =>
|
||||
button.setButtonText('Cancel').onClick(() => {
|
||||
this.close();
|
||||
}),
|
||||
)
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText(this.actionText)
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
void this.submit();
|
||||
}),
|
||||
);
|
||||
|
||||
pathInput?.inputEl.focus();
|
||||
}
|
||||
|
||||
private async submit(): Promise<void> {
|
||||
const normalizedPath = this.path.trim();
|
||||
if (normalizedPath === '' || !isJavaScriptVaultScriptPath(normalizedPath)) {
|
||||
new Notice('Enter a vault path ending in .js.');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await this.onSubmit({
|
||||
name: this.name,
|
||||
path: normalizedPath,
|
||||
runOnStartup: this.runOnStartup,
|
||||
});
|
||||
if (result.message !== undefined) {
|
||||
new Notice(result.message);
|
||||
}
|
||||
if (result.saved) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
53
packages/obsidian/src/ui/settings-tab.ts
Normal file
53
packages/obsidian/src/ui/settings-tab.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { App } from 'obsidian';
|
||||
import { PluginSettingTab } from 'obsidian';
|
||||
import type SafeJsPlugin from 'packages/obsidian/src/main';
|
||||
import type { PermissionSettingsStore } from 'packages/obsidian/src/permissions/approval-store';
|
||||
import {
|
||||
AppPermissionStorage,
|
||||
LocalStoragePermissionApprovalStore,
|
||||
LocalStoragePermissionSettingsStore,
|
||||
} from 'packages/obsidian/src/permissions/approval-store';
|
||||
import type { VaultScriptManager } from 'packages/obsidian/src/scripts/vault-script-manager';
|
||||
import { RuntimeSettingsSection } from 'packages/obsidian/src/ui/settings/runtime-settings-section';
|
||||
import { StoredDataSettingsSection } from 'packages/obsidian/src/ui/settings/stored-data-settings-section';
|
||||
import { VaultScriptsSettingsSection } from 'packages/obsidian/src/ui/settings/vault-scripts-settings-section';
|
||||
|
||||
export class SafeJsSettingTab extends PluginSettingTab {
|
||||
plugin: SafeJsPlugin;
|
||||
private readonly approvalStore: LocalStoragePermissionApprovalStore;
|
||||
private readonly permissionSettingsStore: PermissionSettingsStore;
|
||||
private readonly scriptManager?: VaultScriptManager;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: SafeJsPlugin,
|
||||
permissionSettingsStore: PermissionSettingsStore = new LocalStoragePermissionSettingsStore(new AppPermissionStorage(app)),
|
||||
scriptManager?: VaultScriptManager,
|
||||
) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
this.approvalStore = new LocalStoragePermissionApprovalStore(new AppPermissionStorage(app));
|
||||
this.permissionSettingsStore = permissionSettingsStore;
|
||||
this.scriptManager = scriptManager;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
|
||||
new RuntimeSettingsSection(this.plugin, this.permissionSettingsStore, () => {
|
||||
this.display();
|
||||
}).render(containerEl);
|
||||
new VaultScriptsSettingsSection(
|
||||
this.app,
|
||||
this.plugin,
|
||||
() => {
|
||||
this.display();
|
||||
},
|
||||
this.scriptManager,
|
||||
).render(containerEl);
|
||||
new StoredDataSettingsSection(this.app, this.approvalStore, () => {
|
||||
this.display();
|
||||
}).render(containerEl);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import { Setting } from 'obsidian';
|
||||
import type SafeJsPlugin from 'packages/obsidian/src/main';
|
||||
import type { PermissionSettingsStore } from 'packages/obsidian/src/permissions/approval-store';
|
||||
import type { SafeJsSettings } from 'packages/obsidian/src/settings/settings-schema';
|
||||
import { DEFAULT_SETTINGS } from 'packages/obsidian/src/settings/settings-schema';
|
||||
|
||||
export class RuntimeSettingsSection {
|
||||
private readonly onSettingsChanged: () => void;
|
||||
private readonly permissionSettingsStore: PermissionSettingsStore;
|
||||
private readonly plugin: SafeJsPlugin;
|
||||
|
||||
constructor(plugin: SafeJsPlugin, permissionSettingsStore: PermissionSettingsStore, onSettingsChanged: () => void) {
|
||||
this.onSettingsChanged = onSettingsChanged;
|
||||
this.permissionSettingsStore = permissionSettingsStore;
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
render(containerEl: HTMLElement): void {
|
||||
this.createSection(containerEl, 'Runtime settings');
|
||||
new Setting(containerEl)
|
||||
.setName('Execution timeouts')
|
||||
.setDesc('Cancel scripts that run longer than the configured timeout.')
|
||||
.addToggle(toggle =>
|
||||
toggle.setValue(this.plugin.settings.executionTimeoutsEnabled).onChange(async value => {
|
||||
this.plugin.settings.executionTimeoutsEnabled = value;
|
||||
await this.plugin.saveSettings();
|
||||
this.onSettingsChanged();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Execution timeout')
|
||||
.setDesc(this.executionTimeoutDescription(this.plugin.settings))
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder(String(DEFAULT_SETTINGS.executionTimeoutMs))
|
||||
.setValue(String(this.plugin.settings.executionTimeoutMs))
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.executionTimeoutMs = this.normalizeTimeoutMs(value);
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Debug blocks')
|
||||
.setDesc('Enable support for the safe-js-debug code block language.')
|
||||
.addToggle(toggle =>
|
||||
toggle.setValue(this.plugin.settings.debugBlocksEnabled).onChange(async value => {
|
||||
this.plugin.settings.debugBlocksEnabled = value;
|
||||
await this.plugin.saveSettings();
|
||||
}),
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Auto-allow low-risk permissions')
|
||||
.setDesc('Allow low-risk permissions without prompting. Approvals are still remembered per script hash on this device.')
|
||||
.addToggle(toggle =>
|
||||
toggle.setValue(this.permissionSettingsStore.loadAutoAllowLowRiskPermissions()).onChange(value => {
|
||||
this.permissionSettingsStore.saveAutoAllowLowRiskPermissions(value);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private createSection(containerEl: HTMLElement, heading: string): void {
|
||||
new Setting(containerEl).setName(heading).setHeading();
|
||||
}
|
||||
|
||||
private executionTimeoutDescription(settings: SafeJsSettings): string {
|
||||
return settings.executionTimeoutsEnabled
|
||||
? 'Maximum time a worker-backed script may run before it is cancelled.'
|
||||
: 'Timeouts are disabled. This value is kept for later use.';
|
||||
}
|
||||
|
||||
private normalizeTimeoutMs(value: string): number {
|
||||
const parsedValue = Number.parseInt(value, 10);
|
||||
return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : DEFAULT_SETTINGS.executionTimeoutMs;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
import type { App } from 'obsidian';
|
||||
import { Notice, Setting, SettingGroup } from 'obsidian';
|
||||
import type { PermissionApproval } from 'packages/obsidian/src/permissions/approval-store';
|
||||
import type { LocalStoragePermissionApprovalStore } from 'packages/obsidian/src/permissions/approval-store';
|
||||
import type { ScriptStorageEntry } from 'packages/obsidian/src/storage/script-storage';
|
||||
import { ScriptStorageManager } from 'packages/obsidian/src/storage/script-storage';
|
||||
|
||||
export class StoredDataSettingsSection {
|
||||
private readonly app: App;
|
||||
private readonly approvalStore: LocalStoragePermissionApprovalStore;
|
||||
private readonly onStorageChanged: () => void;
|
||||
private readonly staleEntryAgeMs = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
constructor(app: App, approvalStore: LocalStoragePermissionApprovalStore, onStorageChanged: () => void) {
|
||||
this.app = app;
|
||||
this.approvalStore = approvalStore;
|
||||
this.onStorageChanged = onStorageChanged;
|
||||
}
|
||||
|
||||
render(containerEl: HTMLElement): void {
|
||||
this.renderApprovalStorage(containerEl);
|
||||
this.renderScriptStorage(containerEl);
|
||||
}
|
||||
|
||||
private renderApprovalStorage(containerEl: HTMLElement): void {
|
||||
const approvals = this.approvalStore.list();
|
||||
this.createSection(containerEl, 'Approved script hashes');
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Clear approved hashes')
|
||||
.setDesc('Remove remembered permission approvals for changed or old scripts.')
|
||||
.addButton(button =>
|
||||
button.setButtonText('Clear older than 30 days').onClick(() => {
|
||||
const deletedCount = this.approvalStore.deleteOlderThan(Date.now() - this.staleEntryAgeMs);
|
||||
new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`);
|
||||
this.onStorageChanged();
|
||||
}),
|
||||
)
|
||||
.addButton(button =>
|
||||
button.setButtonText('Clear all').onClick(() => {
|
||||
const deletedCount = this.approvalStore.deleteAll();
|
||||
new Notice(`Cleared ${deletedCount} approved script ${deletedCount === 1 ? 'hash' : 'hashes'}.`);
|
||||
this.onStorageChanged();
|
||||
}),
|
||||
);
|
||||
|
||||
this.renderApprovalList(new SettingGroup(containerEl), approvals);
|
||||
}
|
||||
|
||||
private renderApprovalList(group: SettingGroup, approvals: PermissionApproval[]): void {
|
||||
if (approvals.length === 0) {
|
||||
group.addSetting(setting => {
|
||||
setting.setName('No approved script hashes are stored.');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const approval of approvals.slice(0, 20)) {
|
||||
group.addSetting(setting => {
|
||||
setting
|
||||
.setName(approval.codeHash)
|
||||
.setDesc(`${formatCaller(approval.callerPluginId)} - ${approval.permissions.join(', ')} - ${formatDate(approval.updatedAt)}`)
|
||||
.addButton(button =>
|
||||
button.setButtonText('Revoke').onClick(() => {
|
||||
this.approvalStore.delete(approval);
|
||||
new Notice(`Revoked approval for script hash ${approval.codeHash}.`);
|
||||
this.onStorageChanged();
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (approvals.length > 20) {
|
||||
group.addSetting(setting => {
|
||||
setting.setName(`${approvals.length - 20} more approved hashes are hidden.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private renderScriptStorage(containerEl: HTMLElement): void {
|
||||
const entries = ScriptStorageManager.listAll(this.app);
|
||||
this.createSection(containerEl, 'Script storage');
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Clear script storage')
|
||||
.setDesc('Remove data written through the storage API by scripts.')
|
||||
.addButton(button =>
|
||||
button.setButtonText('Clear older than 30 days').onClick(() => {
|
||||
const deletedCount = ScriptStorageManager.deleteOlderThanAll(this.app, Date.now() - this.staleEntryAgeMs);
|
||||
new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`);
|
||||
this.onStorageChanged();
|
||||
}),
|
||||
)
|
||||
.addButton(button =>
|
||||
button.setButtonText('Clear all').onClick(() => {
|
||||
const deletedCount = ScriptStorageManager.deleteAllKnown(this.app);
|
||||
new Notice(`Cleared ${deletedCount} script storage ${deletedCount === 1 ? 'key' : 'keys'}.`);
|
||||
this.onStorageChanged();
|
||||
}),
|
||||
);
|
||||
|
||||
this.renderScriptStorageList(new SettingGroup(containerEl), entries);
|
||||
}
|
||||
|
||||
private renderScriptStorageList(group: SettingGroup, entries: ScriptStorageEntry[]): void {
|
||||
if (entries.length === 0) {
|
||||
group.addSetting(setting => {
|
||||
setting.setName('No indexed script storage keys are stored.');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entry of entries.slice(0, 20)) {
|
||||
group.addSetting(setting => {
|
||||
setting.setName(entry.key).setDesc(`${formatScope(entry.scope)} - ${formatBytes(entry.sizeBytes)} - ${formatDate(entry.updatedAt)}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (entries.length > 20) {
|
||||
group.addSetting(setting => {
|
||||
setting.setName(`${entries.length - 20} more storage keys are hidden.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private createSection(containerEl: HTMLElement, heading: string): void {
|
||||
new Setting(containerEl).setName(heading).setHeading();
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(timestamp: number): string {
|
||||
return new Date(timestamp).toLocaleString();
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
|
||||
function formatCaller(callerPluginId: string | undefined): string {
|
||||
return callerPluginId ?? 'notes';
|
||||
}
|
||||
|
||||
function formatScope(scope: string | null): string {
|
||||
return scope === null ? 'global' : `scoped ${scope.slice(0, 12)}`;
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
import type { App } from 'obsidian';
|
||||
import { Setting, SettingGroup } from 'obsidian';
|
||||
import type SafeJsPlugin from 'packages/obsidian/src/main';
|
||||
import type { SafeJsScriptConfig } from 'packages/obsidian/src/scripts/script-settings';
|
||||
import { createScriptConfig, displayNameFromPath, isJavaScriptVaultScriptPath } from 'packages/obsidian/src/scripts/script-settings';
|
||||
import type { VaultScriptManager } from 'packages/obsidian/src/scripts/vault-script-manager';
|
||||
import type { AddVaultScriptModalValues } from 'packages/obsidian/src/ui/add-vault-script-modal';
|
||||
import { AddVaultScriptModal } from 'packages/obsidian/src/ui/add-vault-script-modal';
|
||||
|
||||
export class VaultScriptsSettingsSection {
|
||||
private readonly app: App;
|
||||
private readonly onSettingsChanged: () => void;
|
||||
private readonly plugin: SafeJsPlugin;
|
||||
private readonly scriptManager?: VaultScriptManager;
|
||||
|
||||
constructor(app: App, plugin: SafeJsPlugin, onSettingsChanged: () => void, scriptManager?: VaultScriptManager) {
|
||||
this.app = app;
|
||||
this.onSettingsChanged = onSettingsChanged;
|
||||
this.plugin = plugin;
|
||||
this.scriptManager = scriptManager;
|
||||
}
|
||||
|
||||
render(containerEl: HTMLElement): void {
|
||||
this.createSection(containerEl, 'Vault scripts');
|
||||
const group = new SettingGroup(containerEl);
|
||||
|
||||
for (const script of this.plugin.settings.scripts) {
|
||||
this.renderVaultScript(group, script);
|
||||
}
|
||||
|
||||
this.renderAddVaultScript(group);
|
||||
}
|
||||
|
||||
private renderVaultScript(group: SettingGroup, script: SafeJsScriptConfig): void {
|
||||
group.addSetting(setting => {
|
||||
setting
|
||||
.setName(script.name)
|
||||
.setDesc(formatVaultScriptDescription(script))
|
||||
.addButton(button =>
|
||||
button.setButtonText('Edit').onClick(() => {
|
||||
this.openEditVaultScriptModal(script);
|
||||
}),
|
||||
)
|
||||
.addButton(button =>
|
||||
button.setButtonText('Remove').onClick(() => {
|
||||
void this.removeVaultScript(script);
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private renderAddVaultScript(group: SettingGroup): void {
|
||||
group.addSetting(setting => {
|
||||
setting
|
||||
.setName('Add script')
|
||||
.setDesc('Configure a vault .js file as a command.')
|
||||
.addButton(button =>
|
||||
button
|
||||
.setButtonText('Add script')
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.openAddVaultScriptModal();
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private openAddVaultScriptModal(): void {
|
||||
new AddVaultScriptModal(
|
||||
this.app,
|
||||
{
|
||||
actionText: 'Add',
|
||||
initialValues: {
|
||||
name: '',
|
||||
path: '',
|
||||
runOnStartup: false,
|
||||
},
|
||||
title: 'Add script',
|
||||
},
|
||||
async values => await this.addVaultScript(values),
|
||||
).open();
|
||||
}
|
||||
|
||||
private async removeVaultScript(script: SafeJsScriptConfig): Promise<void> {
|
||||
this.plugin.settings.scripts = this.plugin.settings.scripts.filter(candidate => candidate.id !== script.id);
|
||||
await this.saveScriptSettings();
|
||||
this.onSettingsChanged();
|
||||
}
|
||||
|
||||
private openEditVaultScriptModal(script: SafeJsScriptConfig): void {
|
||||
new AddVaultScriptModal(
|
||||
this.app,
|
||||
{
|
||||
actionText: 'Save',
|
||||
initialValues: {
|
||||
name: script.name,
|
||||
path: script.path,
|
||||
runOnStartup: script.runOnStartup,
|
||||
},
|
||||
title: 'Edit script',
|
||||
},
|
||||
async values => await this.editVaultScript(script, values),
|
||||
).open();
|
||||
}
|
||||
|
||||
private async addVaultScript(values: AddVaultScriptModalValues): Promise<{ message?: string; saved: boolean }> {
|
||||
const normalizedPath = values.path.trim();
|
||||
if (normalizedPath === '' || !isJavaScriptVaultScriptPath(normalizedPath)) {
|
||||
return { message: 'Enter a vault path ending in .js.', saved: false };
|
||||
}
|
||||
|
||||
if (this.plugin.settings.scripts.some(script => script.path === normalizedPath)) {
|
||||
return { message: 'That script is already configured.', saved: false };
|
||||
}
|
||||
|
||||
const script = createScriptConfig(normalizedPath, values.name, this.plugin.settings.scripts);
|
||||
script.runOnStartup = values.runOnStartup;
|
||||
this.plugin.settings.scripts = [...this.plugin.settings.scripts, script];
|
||||
await this.saveScriptSettings();
|
||||
this.onSettingsChanged();
|
||||
return { saved: true };
|
||||
}
|
||||
|
||||
private async editVaultScript(script: SafeJsScriptConfig, values: AddVaultScriptModalValues): Promise<{ message?: string; saved: boolean }> {
|
||||
const normalizedPath = values.path.trim();
|
||||
if (normalizedPath === '' || !isJavaScriptVaultScriptPath(normalizedPath)) {
|
||||
return { message: 'Enter a vault path ending in .js.', saved: false };
|
||||
}
|
||||
|
||||
if (this.plugin.settings.scripts.some(candidate => candidate.id !== script.id && candidate.path === normalizedPath)) {
|
||||
return { message: 'That script is already configured.', saved: false };
|
||||
}
|
||||
|
||||
script.name = values.name.trim() || displayNameFromPath(normalizedPath);
|
||||
script.path = normalizedPath;
|
||||
script.runOnStartup = values.runOnStartup;
|
||||
await this.saveScriptSettings();
|
||||
this.onSettingsChanged();
|
||||
return { saved: true };
|
||||
}
|
||||
|
||||
private async saveScriptSettings(): Promise<void> {
|
||||
await this.plugin.saveSettings();
|
||||
this.scriptManager?.reloadCommands();
|
||||
}
|
||||
|
||||
private createSection(containerEl: HTMLElement, heading: string): void {
|
||||
new Setting(containerEl).setName(heading).setHeading();
|
||||
}
|
||||
}
|
||||
|
||||
function formatVaultScriptDescription(script: SafeJsScriptConfig): string {
|
||||
return script.runOnStartup ? `${script.path} - Runs on startup` : script.path;
|
||||
}
|
||||
|
|
@ -53,7 +53,11 @@ function normalizeParams(binding: WorkerRpcBinding, args: unknown[]): JsonValue
|
|||
}
|
||||
|
||||
const params = args[0] ?? {};
|
||||
return isJsonValue(params) ? params : {};
|
||||
if (!isJsonValue(params)) {
|
||||
throw new Error('RPC object parameters must be JSON-safe.');
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function normalizeNamedArgs(binding: WorkerRpcBinding, args: unknown[]): JsonValue {
|
||||
|
|
@ -62,7 +66,11 @@ function normalizeNamedArgs(binding: WorkerRpcBinding, args: unknown[]): JsonVal
|
|||
for (const [index, name] of argNames.entries()) {
|
||||
const value = args[index];
|
||||
if (value !== undefined) {
|
||||
params[name] = isJsonValue(value) ? value : null;
|
||||
if (!isJsonValue(value)) {
|
||||
throw new Error(`RPC argument '${name}' must be JSON-safe.`);
|
||||
}
|
||||
|
||||
params[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -126,8 +126,8 @@ async function waitForWorker(workerFactory: { workers: unknown[] }): Promise<voi
|
|||
}
|
||||
|
||||
function createRegistry(): RpcRegistry {
|
||||
return new RpcRegistry(
|
||||
[
|
||||
return new RpcRegistry({
|
||||
methods: [
|
||||
{
|
||||
method: 'test:echo',
|
||||
permission: 'test:call',
|
||||
|
|
@ -143,9 +143,8 @@ function createRegistry(): RpcRegistry {
|
|||
handler: params => params,
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
testValidatorOptions,
|
||||
);
|
||||
validators: testValidatorOptions,
|
||||
});
|
||||
}
|
||||
|
||||
function createService(options: { promptApproved: boolean; store?: MemoryPermissionApprovalStore; workerFactory?: FakeWorkerFactory }) {
|
||||
|
|
@ -175,8 +174,8 @@ function createLowRiskService(options: { autoAllowLowRiskPermissions: boolean })
|
|||
const workerFactory = new FakeWorkerFactory();
|
||||
const store = new MemoryPermissionApprovalStore();
|
||||
const service = new SafeJsExecutionService({
|
||||
rpcRegistry: new RpcRegistry(
|
||||
[
|
||||
rpcRegistry: new RpcRegistry({
|
||||
methods: [
|
||||
{
|
||||
method: 'test:echo',
|
||||
permission: 'test:call',
|
||||
|
|
@ -192,7 +191,7 @@ function createLowRiskService(options: { autoAllowLowRiskPermissions: boolean })
|
|||
handler: params => params,
|
||||
},
|
||||
],
|
||||
[
|
||||
permissionDefinitions: [
|
||||
{
|
||||
id: 'test:call',
|
||||
name: 'Test calls',
|
||||
|
|
@ -201,8 +200,8 @@ function createLowRiskService(options: { autoAllowLowRiskPermissions: boolean })
|
|||
grantGuidance: 'Grant in tests.',
|
||||
},
|
||||
],
|
||||
testValidatorOptions,
|
||||
),
|
||||
validators: testValidatorOptions,
|
||||
}),
|
||||
approvalStore: store,
|
||||
permissionPrompt: prompt,
|
||||
workerFactory,
|
||||
|
|
@ -426,8 +425,8 @@ test('measures execution elapsed time after permission approval', async () => {
|
|||
})(true);
|
||||
const workerFactory = new FakeWorkerFactory();
|
||||
const service = new SafeJsExecutionService({
|
||||
rpcRegistry: new RpcRegistry(
|
||||
[
|
||||
rpcRegistry: new RpcRegistry({
|
||||
methods: [
|
||||
{
|
||||
method: 'test:echo',
|
||||
permission: 'test:call',
|
||||
|
|
@ -446,9 +445,8 @@ test('measures execution elapsed time after permission approval', async () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
testValidatorOptions,
|
||||
),
|
||||
validators: testValidatorOptions,
|
||||
}),
|
||||
approvalStore: new MemoryPermissionApprovalStore(),
|
||||
permissionPrompt: prompt,
|
||||
workerFactory,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,10 @@ mock.module('obsidian', () => ({
|
|||
|
||||
async function createRegistry(): Promise<RpcRegistry> {
|
||||
const { createHelperMethods } = await import('packages/obsidian/src/rpc/obsidian/helper-rpc');
|
||||
return new RpcRegistry(createHelperMethods(), undefined, testValidatorOptions);
|
||||
return new RpcRegistry({
|
||||
methods: createHelperMethods(),
|
||||
validators: testValidatorOptions,
|
||||
});
|
||||
}
|
||||
|
||||
test('registers helper methods under the helper permission', async () => {
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ function createPlugin(): { plugin: Plugin; unload(): void } {
|
|||
|
||||
test('public caller API stamps execution source with caller plugin metadata', async () => {
|
||||
const executionService = new FakeExecutionService();
|
||||
const registry = new RpcRegistry([], undefined, testValidatorOptions);
|
||||
const registry = new RpcRegistry({ validators: testValidatorOptions });
|
||||
const { plugin } = createPlugin();
|
||||
const safeJsApi = new DefaultSafeJsPublicApi({
|
||||
executionService: executionService as unknown as SafeJsExecutionService,
|
||||
|
|
@ -106,7 +106,7 @@ test('public caller API stamps execution source with caller plugin metadata', as
|
|||
|
||||
test('public caller API registers owned sandbox functions and cleans them up on unload', async () => {
|
||||
const executionService = new FakeExecutionService();
|
||||
const registry = new RpcRegistry([], undefined, testValidatorOptions);
|
||||
const registry = new RpcRegistry({ validators: testValidatorOptions });
|
||||
const { plugin, unload } = createPlugin();
|
||||
const callerApi = new DefaultSafeJsPublicApi({
|
||||
executionService: executionService as unknown as SafeJsExecutionService,
|
||||
|
|
@ -154,7 +154,7 @@ test('public caller API registers owned sandbox functions and cleans them up on
|
|||
|
||||
test('public caller API rejects sandbox functions with unknown permissions', () => {
|
||||
const executionService = new FakeExecutionService();
|
||||
const registry = new RpcRegistry([], undefined, testValidatorOptions);
|
||||
const registry = new RpcRegistry({ validators: testValidatorOptions });
|
||||
const { plugin } = createPlugin();
|
||||
const callerApi = new DefaultSafeJsPublicApi({
|
||||
executionService: executionService as unknown as SafeJsExecutionService,
|
||||
|
|
@ -182,7 +182,7 @@ test('public caller API rejects sandbox functions with unknown permissions', ()
|
|||
|
||||
test('public caller API exposes built-in validator IDs without exposing zod', () => {
|
||||
const executionService = new FakeExecutionService();
|
||||
const registry = new RpcRegistry([], undefined, testValidatorOptions);
|
||||
const registry = new RpcRegistry({ validators: testValidatorOptions });
|
||||
const { plugin } = createPlugin();
|
||||
const callerApi = new DefaultSafeJsPublicApi({
|
||||
executionService: executionService as unknown as SafeJsExecutionService,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ function echoValueValidator(value: unknown): SafeJsValidationResult<EchoValue> {
|
|||
}
|
||||
|
||||
function createTestRegistry(): RpcRegistry {
|
||||
return new RpcRegistry(
|
||||
[
|
||||
return new RpcRegistry({
|
||||
methods: [
|
||||
{
|
||||
method: 'test:echo',
|
||||
permission: 'test:call',
|
||||
|
|
@ -38,9 +38,8 @@ function createTestRegistry(): RpcRegistry {
|
|||
handler: params => params,
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
testValidatorOptions,
|
||||
);
|
||||
validators: testValidatorOptions,
|
||||
});
|
||||
}
|
||||
|
||||
test('dispatches valid RPC calls', async () => {
|
||||
|
|
@ -94,8 +93,8 @@ test('exposes worker bindings with argument metadata', () => {
|
|||
});
|
||||
|
||||
test('generates docs from registered permission and method definitions', () => {
|
||||
const registry = new RpcRegistry(
|
||||
[
|
||||
const registry = new RpcRegistry({
|
||||
methods: [
|
||||
{
|
||||
method: 'test:echo',
|
||||
permission: 'test:call',
|
||||
|
|
@ -111,7 +110,7 @@ test('generates docs from registered permission and method definitions', () => {
|
|||
handler: params => params,
|
||||
},
|
||||
],
|
||||
[
|
||||
permissionDefinitions: [
|
||||
{
|
||||
id: 'test:call',
|
||||
name: 'Test calls',
|
||||
|
|
@ -120,8 +119,8 @@ test('generates docs from registered permission and method definitions', () => {
|
|||
grantGuidance: 'Grant in tests.',
|
||||
},
|
||||
],
|
||||
testValidatorOptions,
|
||||
);
|
||||
validators: testValidatorOptions,
|
||||
});
|
||||
const docs = registry.getDocs();
|
||||
|
||||
expect(docs).toHaveLength(1);
|
||||
|
|
@ -130,7 +129,7 @@ test('generates docs from registered permission and method definitions', () => {
|
|||
});
|
||||
|
||||
test('registers owned custom permissions, methods, and globals', () => {
|
||||
const registry = new RpcRegistry([], undefined, testValidatorOptions);
|
||||
const registry = new RpcRegistry({ validators: testValidatorOptions });
|
||||
registry.registerPermission(
|
||||
{
|
||||
id: 'plugin:call',
|
||||
|
|
@ -201,7 +200,7 @@ test('registers owned custom permissions, methods, and globals', () => {
|
|||
});
|
||||
|
||||
test('exposes standalone built-in permissions', () => {
|
||||
const registry = new RpcRegistry([], undefined, testValidatorOptions);
|
||||
const registry = new RpcRegistry({ validators: testValidatorOptions });
|
||||
|
||||
expect(registry.getKnownPermissions()).toContain('output:render-rich');
|
||||
expect(registry.getDocs().find(group => group.permission.id === 'output:render-rich')).toMatchObject({
|
||||
|
|
@ -244,7 +243,7 @@ test('rejects duplicate sandbox API paths and reserved globals', () => {
|
|||
});
|
||||
|
||||
test('rejects sandbox globals with non-json values', () => {
|
||||
const registry = new RpcRegistry([], undefined, testValidatorOptions);
|
||||
const registry = new RpcRegistry({ validators: testValidatorOptions });
|
||||
|
||||
expect(() => {
|
||||
registry.registerSandboxGlobal({
|
||||
|
|
@ -256,7 +255,7 @@ test('rejects sandbox globals with non-json values', () => {
|
|||
});
|
||||
|
||||
test('keeps owned permissions while sandbox globals still reference them', () => {
|
||||
const registry = new RpcRegistry([], undefined, testValidatorOptions);
|
||||
const registry = new RpcRegistry({ validators: testValidatorOptions });
|
||||
registry.registerPermission(
|
||||
{
|
||||
id: 'plugin:shared',
|
||||
|
|
@ -285,8 +284,8 @@ test('keeps owned permissions while sandbox globals still reference them', () =>
|
|||
});
|
||||
|
||||
test('dispatches methods that reference built-in validators by id', async () => {
|
||||
const registry = new RpcRegistry(
|
||||
[
|
||||
const registry = new RpcRegistry({
|
||||
methods: [
|
||||
{
|
||||
method: 'test:ping',
|
||||
permission: 'test:call',
|
||||
|
|
@ -302,9 +301,8 @@ test('dispatches methods that reference built-in validators by id', async () =>
|
|||
handler: () => ({ ok: true }),
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
testValidatorOptions,
|
||||
);
|
||||
validators: testValidatorOptions,
|
||||
});
|
||||
|
||||
expect(await registry.dispatch('test:ping', {}, { grantedPermissions: new Set(['test:call']) })).toEqual({
|
||||
ok: true,
|
||||
|
|
@ -315,7 +313,7 @@ test('dispatches methods that reference built-in validators by id', async () =>
|
|||
|
||||
test('built-in vault path validators read the current config directory', () => {
|
||||
let configDir = '_config-a';
|
||||
const registry = new RpcRegistry([], undefined, createBuiltInValidators({ getConfigDir: () => configDir }));
|
||||
const registry = new RpcRegistry({ validators: createBuiltInValidators({ getConfigDir: () => configDir }) });
|
||||
|
||||
expect(registry.validate('vault:path', '_config-a/plugins/safe-js/data.json')).toMatchObject({
|
||||
success: false,
|
||||
|
|
@ -338,8 +336,8 @@ test('built-in vault path validators read the current config directory', () => {
|
|||
|
||||
test('rejects methods that reference unknown validator ids', () => {
|
||||
expect(() => {
|
||||
new RpcRegistry(
|
||||
[
|
||||
new RpcRegistry({
|
||||
methods: [
|
||||
{
|
||||
method: 'test:bad',
|
||||
permission: 'test:call',
|
||||
|
|
@ -355,8 +353,7 @@ test('rejects methods that reference unknown validator ids', () => {
|
|||
handler: () => ({ ok: true }),
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
testValidatorOptions,
|
||||
);
|
||||
validators: testValidatorOptions,
|
||||
});
|
||||
}).toThrow("Unknown request validator 'missing:validator'");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ function createSession(workerFactory: WorkerFactory, registry: RpcRegistry): Wor
|
|||
}
|
||||
|
||||
function createRegistry(): RpcRegistry {
|
||||
return new RpcRegistry(
|
||||
[
|
||||
return new RpcRegistry({
|
||||
methods: [
|
||||
{
|
||||
method: 'vault:read',
|
||||
permission: 'vault:read',
|
||||
|
|
@ -97,9 +97,8 @@ function createRegistry(): RpcRegistry {
|
|||
},
|
||||
},
|
||||
],
|
||||
undefined,
|
||||
testValidatorOptions,
|
||||
);
|
||||
validators: testValidatorOptions,
|
||||
});
|
||||
}
|
||||
|
||||
test('sandbox global documentation only advertises the intentional host surface', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue