mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-06-08 07:13:53 +02:00
v0.9.6: InfoNet hashchain, Wormhole gate encryption, mesh reputation, 16 community contributors
Gate messages now propagate via the Infonet hashchain as encrypted blobs — every node syncs them through normal chain sync while only Gate members with MLS keys can decrypt. Added mesh reputation system, peer push workers, voluntary Wormhole opt-in for node participation, fork recovery, killwormhole scripts, obfuscated terminology, and hardened the self-updater to protect encryption keys and chain state during updates. New features: Shodan search, train tracking, Sentinel Hub imagery, 8 new intelligence layers, CCTV expansion to 11,000+ cameras across 6 countries, Mesh Terminal CLI, prediction markets, desktop-shell scaffold, and comprehensive mesh test suite (215 frontend + backend tests passing). Community contributors: @wa1id, @AlborzNazari, @adust09, @Xpirix, @imqdcr, @csysp, @suranyami, @chr0n1x, @johan-martensson, @singularfailure, @smithbh, @OrfeoTerkuci, @deuza, @tm-const, @Elhard1, @ttulttul
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# Desktop Shell Scaffold
|
||||
|
||||
This folder is the first native-side scaffold for the staged desktop boundary.
|
||||
|
||||
## Purpose
|
||||
|
||||
It gives the future Tauri/native shell a concrete shape for:
|
||||
|
||||
- command routing
|
||||
- handler grouping
|
||||
- runtime bridge installation
|
||||
|
||||
without forcing a packaging migration yet.
|
||||
|
||||
## Source of truth
|
||||
|
||||
The shared desktop control contract still lives in:
|
||||
|
||||
- `F:\Codebase\Oracle\live-risk-dashboard\frontend\src\lib\desktopControlContract.ts`
|
||||
|
||||
The native-side scaffold imports that contract rather than redefining it.
|
||||
|
||||
## First command scope
|
||||
|
||||
The initial native command set covers only:
|
||||
|
||||
- Wormhole lifecycle
|
||||
- protected settings get/set
|
||||
- update trigger
|
||||
|
||||
That is deliberate. The goal is to move the local privileged control plane first, not the entire
|
||||
mesh data plane.
|
||||
|
||||
## Scaffold layout
|
||||
|
||||
- `F:\Codebase\Oracle\live-risk-dashboard\desktop-shell\src\types.ts`
|
||||
- `F:\Codebase\Oracle\live-risk-dashboard\desktop-shell\src\handlers\wormholeHandlers.ts`
|
||||
- `F:\Codebase\Oracle\live-risk-dashboard\desktop-shell\src\handlers\settingsHandlers.ts`
|
||||
- `F:\Codebase\Oracle\live-risk-dashboard\desktop-shell\src\handlers\updateHandlers.ts`
|
||||
- `F:\Codebase\Oracle\live-risk-dashboard\desktop-shell\src\nativeControlRouter.ts`
|
||||
- `F:\Codebase\Oracle\live-risk-dashboard\desktop-shell\src\runtimeBridge.ts`
|
||||
|
||||
## How to use later
|
||||
|
||||
When the Tauri shell is introduced, its command layer should:
|
||||
|
||||
1. receive `invokeLocalControl(command, payload)`
|
||||
2. dispatch through `createNativeControlRouter(...)`
|
||||
3. return the handler result back to the frontend bridge
|
||||
|
||||
This keeps the frontend contract stable while shifting privileged ownership into the native shell.
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { NativeControlHandlerMap } from '../types';
|
||||
|
||||
export function createSettingsHandlers(): Pick<
|
||||
NativeControlHandlerMap,
|
||||
| 'settings.wormhole.get'
|
||||
| 'settings.wormhole.set'
|
||||
| 'settings.privacy.get'
|
||||
| 'settings.privacy.set'
|
||||
| 'settings.api_keys.get'
|
||||
| 'settings.api_keys.set'
|
||||
| 'settings.news.get'
|
||||
| 'settings.news.set'
|
||||
| 'settings.news.reset'
|
||||
> {
|
||||
return {
|
||||
'settings.wormhole.get': async (_payload, _ctx, exec) => exec('/api/settings/wormhole'),
|
||||
'settings.wormhole.set': async (payload, _ctx, exec) =>
|
||||
exec('/api/settings/wormhole', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'settings.privacy.get': async (_payload, _ctx, exec) =>
|
||||
exec('/api/settings/privacy-profile'),
|
||||
'settings.privacy.set': async (payload, _ctx, exec) =>
|
||||
exec('/api/settings/privacy-profile', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'settings.api_keys.get': async (_payload, _ctx, exec) => exec('/api/settings/api-keys'),
|
||||
'settings.api_keys.set': async (payload, _ctx, exec) =>
|
||||
exec('/api/settings/api-keys', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'settings.news.get': async (_payload, _ctx, exec) => exec('/api/settings/news-feeds'),
|
||||
'settings.news.set': async (payload, _ctx, exec) =>
|
||||
exec('/api/settings/news-feeds', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'settings.news.reset': async (_payload, _ctx, exec) =>
|
||||
exec('/api/settings/news-feeds/reset', {
|
||||
method: 'POST',
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { NativeControlHandlerMap } from '../types';
|
||||
|
||||
export function createUpdateHandlers(): Pick<NativeControlHandlerMap, 'system.update'> {
|
||||
return {
|
||||
'system.update': async (_payload, _ctx, exec) =>
|
||||
exec('/api/system/update', {
|
||||
method: 'POST',
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { NativeControlHandlerMap } from '../types';
|
||||
|
||||
export function createWormholeHandlers(): Pick<
|
||||
NativeControlHandlerMap,
|
||||
| 'wormhole.status'
|
||||
| 'wormhole.connect'
|
||||
| 'wormhole.disconnect'
|
||||
| 'wormhole.restart'
|
||||
| 'wormhole.gate.enter'
|
||||
| 'wormhole.gate.leave'
|
||||
| 'wormhole.gate.proof'
|
||||
| 'wormhole.gate.personas.get'
|
||||
| 'wormhole.gate.persona.create'
|
||||
| 'wormhole.gate.persona.activate'
|
||||
| 'wormhole.gate.persona.clear'
|
||||
| 'wormhole.gate.key.get'
|
||||
| 'wormhole.gate.key.rotate'
|
||||
| 'wormhole.gate.message.compose'
|
||||
| 'wormhole.gate.message.decrypt'
|
||||
| 'wormhole.gate.message.post'
|
||||
| 'wormhole.gate.messages.decrypt'
|
||||
> {
|
||||
return {
|
||||
'wormhole.status': async (_payload, _ctx, exec) => exec('/api/wormhole/status'),
|
||||
'wormhole.connect': async (_payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/connect', { method: 'POST' }),
|
||||
'wormhole.disconnect': async (_payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/disconnect', { method: 'POST' }),
|
||||
'wormhole.restart': async (_payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/restart', { method: 'POST' }),
|
||||
'wormhole.gate.personas.get': async (payload, _ctx, exec) =>
|
||||
exec(`/api/wormhole/gate/${encodeURIComponent(String(payload?.gate_id || ''))}/personas`),
|
||||
'wormhole.gate.persona.create': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/persona/create', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'wormhole.gate.persona.activate': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/persona/activate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'wormhole.gate.persona.clear': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/persona/clear', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'wormhole.gate.key.get': async (payload, _ctx, exec) =>
|
||||
exec(`/api/wormhole/gate/${encodeURIComponent(String(payload?.gate_id || ''))}/key`),
|
||||
'wormhole.gate.key.rotate': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/key/rotate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'wormhole.gate.message.compose': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/message/compose', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'wormhole.gate.message.decrypt': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/message/decrypt', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'wormhole.gate.enter': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/enter', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'wormhole.gate.leave': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/leave', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'wormhole.gate.proof': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/proof', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'wormhole.gate.message.post': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/message/post', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
'wormhole.gate.messages.decrypt': async (payload, _ctx, exec) =>
|
||||
exec('/api/wormhole/gate/messages/decrypt', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './nativeControlRouter';
|
||||
export * from './runtimeBridge';
|
||||
export * from './nativeControlAudit';
|
||||
export * from './types';
|
||||
@@ -0,0 +1,60 @@
|
||||
import type {
|
||||
DesktopControlAuditOutcome,
|
||||
DesktopControlAuditRecord,
|
||||
DesktopControlAuditReport,
|
||||
} from '../../frontend/src/lib/desktopControlContract';
|
||||
import type { NativeControlAuditEvent, NativeControlAuditTrail } from './types';
|
||||
|
||||
const DEFAULT_LIMIT = 100;
|
||||
|
||||
function incrementOutcome(
|
||||
counts: Partial<Record<DesktopControlAuditOutcome, number>>,
|
||||
outcome: DesktopControlAuditOutcome,
|
||||
) {
|
||||
counts[outcome] = (counts[outcome] || 0) + 1;
|
||||
}
|
||||
|
||||
export function createNativeControlAuditTrail(maxEntries: number = DEFAULT_LIMIT): NativeControlAuditTrail {
|
||||
const entries: DesktopControlAuditRecord[] = [];
|
||||
let totalRecorded = 0;
|
||||
|
||||
return {
|
||||
record(event: NativeControlAuditEvent) {
|
||||
totalRecorded += 1;
|
||||
entries.push({
|
||||
...event,
|
||||
recordedAt: Date.now(),
|
||||
});
|
||||
if (entries.length > maxEntries) {
|
||||
entries.splice(0, entries.length - maxEntries);
|
||||
}
|
||||
},
|
||||
snapshot(limit: number = 25): DesktopControlAuditReport {
|
||||
const recent = entries.slice(-Math.max(1, limit)).reverse();
|
||||
const byOutcome: Partial<Record<DesktopControlAuditOutcome, number>> = {};
|
||||
let lastProfileMismatch: DesktopControlAuditRecord | undefined;
|
||||
let lastDenied: DesktopControlAuditRecord | undefined;
|
||||
for (const entry of entries) {
|
||||
incrementOutcome(byOutcome, entry.outcome);
|
||||
if (entry.outcome === 'profile_warn' || entry.outcome === 'profile_denied') {
|
||||
lastProfileMismatch = entry;
|
||||
}
|
||||
if (entry.outcome === 'profile_denied' || entry.outcome === 'capability_denied') {
|
||||
lastDenied = entry;
|
||||
}
|
||||
}
|
||||
return {
|
||||
totalEvents: entries.length,
|
||||
totalRecorded,
|
||||
recent,
|
||||
byOutcome,
|
||||
lastProfileMismatch,
|
||||
lastDenied,
|
||||
};
|
||||
},
|
||||
clear() {
|
||||
totalRecorded = 0;
|
||||
entries.splice(0, entries.length);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import type {
|
||||
DesktopControlCommand,
|
||||
DesktopControlPayloadMap,
|
||||
} from '../../frontend/src/lib/desktopControlContract';
|
||||
import {
|
||||
controlCommandCapability as resolveCommandCapability,
|
||||
extractGateTargetRef,
|
||||
sessionProfileCapabilities as capabilitiesForProfile,
|
||||
} from '../../frontend/src/lib/desktopControlContract';
|
||||
import { createSettingsHandlers } from './handlers/settingsHandlers';
|
||||
import { createUpdateHandlers } from './handlers/updateHandlers';
|
||||
import { createWormholeHandlers } from './handlers/wormholeHandlers';
|
||||
import type {
|
||||
NativeControlExecutor,
|
||||
NativeControlHandlerContext,
|
||||
NativeControlHandlerMap,
|
||||
NativeControlInvokeMeta,
|
||||
} from './types';
|
||||
|
||||
function createHandlerMap(): NativeControlHandlerMap {
|
||||
return {
|
||||
...createWormholeHandlers(),
|
||||
...createSettingsHandlers(),
|
||||
...createUpdateHandlers(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createNativeControlRouter(
|
||||
ctx: NativeControlHandlerContext,
|
||||
exec: NativeControlExecutor,
|
||||
) {
|
||||
const handlers = createHandlerMap();
|
||||
return {
|
||||
async invoke<C extends DesktopControlCommand>(
|
||||
command: C,
|
||||
payload: DesktopControlPayloadMap[C],
|
||||
meta?: NativeControlInvokeMeta,
|
||||
): Promise<unknown> {
|
||||
const handler = handlers[command];
|
||||
if (!handler) {
|
||||
throw new Error(`native_control_handler_missing:${command}`);
|
||||
}
|
||||
const expectedCapability = resolveCommandCapability(command);
|
||||
const profile = ctx.sessionProfile;
|
||||
const profileCapabilities = profile ? capabilitiesForProfile(profile) : [];
|
||||
const profileAllows =
|
||||
!profile || profileCapabilities.length === 0 || profileCapabilities.includes(expectedCapability);
|
||||
const profileEnforced = Boolean((ctx.enforceSessionProfile || meta?.enforceProfileHint) && profile);
|
||||
const allowedCapabilitiesConfigured =
|
||||
Array.isArray(ctx.allowedCapabilities) && ctx.allowedCapabilities.length > 0;
|
||||
const capabilityDenied =
|
||||
allowedCapabilitiesConfigured && !ctx.allowedCapabilities!.includes(expectedCapability);
|
||||
const targetRef = extractGateTargetRef(command, payload);
|
||||
const auditBase = {
|
||||
command,
|
||||
expectedCapability,
|
||||
declaredCapability: meta?.capability,
|
||||
...(targetRef ? { targetRef } : {}),
|
||||
sessionProfile: profile,
|
||||
sessionProfileHint: meta?.sessionProfileHint,
|
||||
enforceProfileHint: meta?.enforceProfileHint,
|
||||
profileAllows,
|
||||
allowedCapabilitiesConfigured,
|
||||
enforced: profileEnforced,
|
||||
} as const;
|
||||
if (meta?.capability && meta.capability !== expectedCapability) {
|
||||
ctx.auditControlUse?.({
|
||||
...auditBase,
|
||||
outcome: 'capability_mismatch',
|
||||
});
|
||||
throw new Error(
|
||||
`native_control_capability_mismatch:${meta.capability}:${expectedCapability}`,
|
||||
);
|
||||
}
|
||||
if (capabilityDenied) {
|
||||
ctx.auditControlUse?.({
|
||||
...auditBase,
|
||||
outcome: 'capability_denied',
|
||||
});
|
||||
throw new Error(`native_control_capability_denied:${expectedCapability}`);
|
||||
}
|
||||
if (!profileAllows) {
|
||||
const profileMessage = `native_control_profile_mismatch:${profile}:${expectedCapability}`;
|
||||
ctx.auditControlUse?.({
|
||||
...auditBase,
|
||||
outcome: profileEnforced ? 'profile_denied' : 'profile_warn',
|
||||
});
|
||||
if (profileEnforced) {
|
||||
throw new Error(profileMessage);
|
||||
}
|
||||
console.warn(profileMessage, {
|
||||
command,
|
||||
sessionProfileHint: meta?.sessionProfileHint,
|
||||
});
|
||||
}
|
||||
if (profileAllows) {
|
||||
ctx.auditControlUse?.({
|
||||
...auditBase,
|
||||
outcome: 'allowed',
|
||||
});
|
||||
}
|
||||
return handler(payload, ctx, exec);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type {
|
||||
DesktopControlCommand,
|
||||
DesktopControlPayloadMap,
|
||||
LocalControlInvokeMeta,
|
||||
} from '../../frontend/src/lib/desktopControlContract';
|
||||
import { createNativeControlAuditTrail } from './nativeControlAudit';
|
||||
import { createNativeControlRouter } from './nativeControlRouter';
|
||||
import type {
|
||||
NativeControlAuditEvent,
|
||||
NativeControlExecutor,
|
||||
NativeControlHandlerContext,
|
||||
} from './types';
|
||||
|
||||
async function defaultExecutor<T = unknown>(baseUrl: string, path: string, init: RequestInit = {}): Promise<T> {
|
||||
const res = await fetch(`${baseUrl}${path}`, init);
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || data?.ok === false) {
|
||||
throw new Error(data?.detail || data?.message || 'native_control_request_failed');
|
||||
}
|
||||
return data as T;
|
||||
}
|
||||
|
||||
export function createRuntimeBridge(ctx: NativeControlHandlerContext) {
|
||||
const auditTrail = ctx.auditTrail || createNativeControlAuditTrail();
|
||||
const auditControlUse = (event: NativeControlAuditEvent) => {
|
||||
auditTrail.record(event);
|
||||
ctx.auditControlUse?.(event);
|
||||
};
|
||||
const exec: NativeControlExecutor = <T = unknown>(path: string, init: RequestInit = {}) => {
|
||||
const headers = new Headers(init.headers);
|
||||
if (ctx.adminKey && !headers.has('X-Admin-Key')) {
|
||||
headers.set('X-Admin-Key', ctx.adminKey);
|
||||
}
|
||||
return defaultExecutor<T>(ctx.backendBaseUrl, path, { ...init, headers });
|
||||
};
|
||||
function invocationContext(meta?: LocalControlInvokeMeta): NativeControlHandlerContext {
|
||||
const baseCtx: NativeControlHandlerContext = {
|
||||
...ctx,
|
||||
auditTrail,
|
||||
auditControlUse,
|
||||
};
|
||||
if (ctx.sessionProfile || !meta?.sessionProfileHint) {
|
||||
return baseCtx;
|
||||
}
|
||||
return {
|
||||
...baseCtx,
|
||||
sessionProfile: meta.sessionProfileHint,
|
||||
};
|
||||
}
|
||||
return {
|
||||
invokeLocalControl<C extends DesktopControlCommand>(
|
||||
command: C,
|
||||
payload: DesktopControlPayloadMap[C],
|
||||
meta?: LocalControlInvokeMeta,
|
||||
) {
|
||||
return createNativeControlRouter(invocationContext(meta), exec).invoke(command, payload, meta);
|
||||
},
|
||||
getNativeControlAuditReport(limit?: number) {
|
||||
return auditTrail.snapshot(limit);
|
||||
},
|
||||
clearNativeControlAuditReport() {
|
||||
auditTrail.clear();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type {
|
||||
DesktopControlAuditEvent,
|
||||
DesktopControlAuditReport,
|
||||
DesktopControlCapability,
|
||||
DesktopControlCommand,
|
||||
DesktopControlPayloadMap,
|
||||
DesktopControlSessionProfile,
|
||||
LocalControlInvokeMeta,
|
||||
} from '../../frontend/src/lib/desktopControlContract';
|
||||
|
||||
export type NativeControlHandlerContext = {
|
||||
backendBaseUrl: string;
|
||||
wormholeBaseUrl: string;
|
||||
adminKey?: string;
|
||||
allowedCapabilities?: DesktopControlCapability[];
|
||||
sessionProfile?: DesktopControlSessionProfile;
|
||||
enforceSessionProfile?: boolean;
|
||||
auditControlUse?: (event: NativeControlAuditEvent) => void;
|
||||
auditTrail?: NativeControlAuditTrail;
|
||||
};
|
||||
|
||||
export type NativeControlExecutor = <T = unknown>(
|
||||
path: string,
|
||||
init?: RequestInit,
|
||||
) => Promise<T>;
|
||||
|
||||
export type NativeControlHandlerMap = {
|
||||
[K in DesktopControlCommand]: (
|
||||
payload: DesktopControlPayloadMap[K],
|
||||
ctx: NativeControlHandlerContext,
|
||||
exec: NativeControlExecutor,
|
||||
) => Promise<unknown>;
|
||||
};
|
||||
|
||||
export type NativeControlInvokeMeta = LocalControlInvokeMeta;
|
||||
|
||||
export type NativeControlAuditEvent = DesktopControlAuditEvent;
|
||||
|
||||
export type NativeControlAuditTrail = {
|
||||
record: (event: NativeControlAuditEvent) => void;
|
||||
snapshot: (limit?: number) => DesktopControlAuditReport;
|
||||
clear: () => void;
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
# Tauri Skeleton
|
||||
|
||||
This folder is the first concrete Tauri-side integration skeleton for the desktop boundary.
|
||||
|
||||
## Scope
|
||||
|
||||
It is intentionally limited to the first trusted local control-plane command set:
|
||||
|
||||
- Wormhole lifecycle
|
||||
- protected settings reads/writes
|
||||
- update trigger
|
||||
|
||||
It does **not** attempt to move DM/data-plane operations yet.
|
||||
|
||||
## What this scaffold demonstrates
|
||||
|
||||
1. a native `invoke_local_control` command entrypoint
|
||||
2. a small Rust-side router for the first command set
|
||||
3. backend HTTP delegation with native-side admin-key ownership
|
||||
4. a simple webview runtime injection path for:
|
||||
- `window.__SHADOWBROKER_DESKTOP__.invokeLocalControl(...)`
|
||||
|
||||
## Important note
|
||||
|
||||
This is a scaffold, not a fully integrated desktop app yet. It exists so the next Tauri pass has a
|
||||
clear structure instead of starting from scratch.
|
||||
|
||||
## Shared contract
|
||||
|
||||
The command names this scaffold must track are defined in:
|
||||
|
||||
- `F:\Codebase\Oracle\live-risk-dashboard\frontend\src\lib\desktopControlContract.ts`
|
||||
- `F:\Codebase\Oracle\live-risk-dashboard\frontend\src\lib\desktopControlRouting.ts`
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "shadowbroker-tauri-shell"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2" }
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tauri = { version = "2", features = [] }
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use serde_json::Value;
|
||||
use tauri::State;
|
||||
|
||||
use crate::{handlers::dispatch_control_command, DesktopAppState};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn invoke_local_control(
|
||||
command: String,
|
||||
payload: Option<Value>,
|
||||
state: State<'_, DesktopAppState>,
|
||||
) -> Result<Value, String> {
|
||||
dispatch_control_command(
|
||||
&state.backend_base_url,
|
||||
state.admin_key.as_deref(),
|
||||
&command,
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use reqwest::Method;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::http_client::call_backend_json;
|
||||
|
||||
pub async fn dispatch_control_command(
|
||||
backend_base_url: &str,
|
||||
admin_key: Option<&str>,
|
||||
command: &str,
|
||||
payload: Option<Value>,
|
||||
) -> Result<Value, String> {
|
||||
match command {
|
||||
"wormhole.status" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/wormhole/status", Method::GET, None).await
|
||||
}
|
||||
"wormhole.connect" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/wormhole/connect", Method::POST, None).await
|
||||
}
|
||||
"wormhole.disconnect" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/wormhole/disconnect", Method::POST, None).await
|
||||
}
|
||||
"wormhole.restart" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/wormhole/restart", Method::POST, None).await
|
||||
}
|
||||
"settings.wormhole.get" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/settings/wormhole", Method::GET, None).await
|
||||
}
|
||||
"settings.wormhole.set" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/settings/wormhole", Method::PUT, payload).await
|
||||
}
|
||||
"settings.privacy.get" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/settings/privacy-profile", Method::GET, None).await
|
||||
}
|
||||
"settings.privacy.set" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/settings/privacy-profile", Method::PUT, payload).await
|
||||
}
|
||||
"settings.api_keys.get" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/settings/api-keys", Method::GET, None).await
|
||||
}
|
||||
"settings.api_keys.set" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/settings/api-keys", Method::PUT, payload).await
|
||||
}
|
||||
"settings.news.get" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/settings/news-feeds", Method::GET, None).await
|
||||
}
|
||||
"settings.news.set" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/settings/news-feeds", Method::PUT, payload).await
|
||||
}
|
||||
"settings.news.reset" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/settings/news-feeds/reset", Method::POST, None).await
|
||||
}
|
||||
"system.update" => {
|
||||
call_backend_json(backend_base_url, admin_key, "/api/system/update", Method::POST, None).await
|
||||
}
|
||||
_ => Err(format!("unsupported_control_command:{command}")),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use reqwest::Method;
|
||||
use serde_json::Value;
|
||||
|
||||
pub async fn call_backend_json(
|
||||
base_url: &str,
|
||||
admin_key: Option<&str>,
|
||||
path: &str,
|
||||
method: Method,
|
||||
payload: Option<Value>,
|
||||
) -> Result<Value, String> {
|
||||
let client = reqwest::Client::new();
|
||||
let mut request = client.request(method, format!("{base_url}{path}"));
|
||||
if let Some(key) = admin_key {
|
||||
if !key.trim().is_empty() {
|
||||
request = request.header("X-Admin-Key", key);
|
||||
}
|
||||
}
|
||||
if let Some(value) = payload {
|
||||
request = request.json(&value);
|
||||
}
|
||||
let response = request
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| format!("backend_request_failed:{e}"))?;
|
||||
let status = response.status();
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| format!("backend_response_failed:{e}"))?;
|
||||
let value: Value = serde_json::from_str(&text).unwrap_or_else(|_| serde_json::json!({}));
|
||||
if !status.is_success() || value.get("ok") == Some(&Value::Bool(false)) {
|
||||
let detail = value
|
||||
.get("detail")
|
||||
.and_then(|v| v.as_str())
|
||||
.or_else(|| value.get("message").and_then(|v| v.as_str()))
|
||||
.unwrap_or("native_control_request_failed");
|
||||
return Err(detail.to_string());
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
mod bridge;
|
||||
mod handlers;
|
||||
mod http_client;
|
||||
|
||||
use bridge::invoke_local_control;
|
||||
|
||||
pub struct DesktopAppState {
|
||||
pub backend_base_url: String,
|
||||
pub admin_key: Option<String>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let backend_base_url =
|
||||
std::env::var("SHADOWBROKER_BACKEND_URL").unwrap_or_else(|_| "http://127.0.0.1:8000".to_string());
|
||||
let admin_key = std::env::var("SHADOWBROKER_ADMIN_KEY").ok();
|
||||
|
||||
tauri::Builder::default()
|
||||
.manage(DesktopAppState {
|
||||
backend_base_url,
|
||||
admin_key,
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![invoke_local_control])
|
||||
.setup(|app| {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let script = r#"
|
||||
window.__SHADOWBROKER_DESKTOP__ = {
|
||||
invokeLocalControl: (command, payload) =>
|
||||
window.__TAURI__.core.invoke('invoke_local_control', { command, payload })
|
||||
};
|
||||
"#;
|
||||
let _ = window.eval(script);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("failed to run shadowbroker tauri shell");
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ShadowBroker Desktop Shell",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.shadowbroker.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../../frontend/.next",
|
||||
"devUrl": "http://127.0.0.1:3000"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "ShadowBroker",
|
||||
"width": 1600,
|
||||
"height": 1000,
|
||||
"resizable": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["ES2022", "DOM"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"../frontend/src/lib/desktopControlContract.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user