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:
anoracleofra-code
2026-03-26 05:58:04 -06:00
parent d363013742
commit 668ce16dc7
363 changed files with 170430 additions and 23203 deletions
-31
View File
@@ -1,31 +0,0 @@
"use client";
import React, { createContext, useContext } from "react";
import type { DashboardData } from "@/types/dashboard";
interface DashboardDataContextValue {
data: DashboardData;
selectedEntity: { id: string | number; type: string; extra?: any } | null;
setSelectedEntity: (entity: { id: string | number; type: string; extra?: any } | null) => void;
}
const DashboardDataContext = createContext<DashboardDataContextValue | null>(null);
export function DashboardDataProvider({
data,
selectedEntity,
setSelectedEntity,
children,
}: DashboardDataContextValue & { children: React.ReactNode }) {
return (
<DashboardDataContext.Provider value={{ data, selectedEntity, setSelectedEntity }}>
{children}
</DashboardDataContext.Provider>
);
}
export function useDashboardData(): DashboardDataContextValue {
const ctx = useContext(DashboardDataContext);
if (!ctx) throw new Error("useDashboardData must be used within DashboardDataProvider");
return ctx;
}
+20 -20
View File
@@ -1,9 +1,9 @@
"use client";
'use client';
import React, { createContext, useContext, useState, useEffect } from "react";
import React, { createContext, useContext, useState, useEffect } from 'react';
type Theme = "dark" | "light";
type HudColor = "cyan" | "matrix";
type Theme = 'dark' | 'light';
type HudColor = 'cyan' | 'matrix';
const ThemeContext = createContext<{
theme: Theme;
@@ -11,41 +11,41 @@ const ThemeContext = createContext<{
hudColor: HudColor;
cycleHudColor: () => void;
}>({
theme: "dark",
theme: 'dark',
toggleTheme: () => {},
hudColor: "cyan",
hudColor: 'cyan',
cycleHudColor: () => {},
});
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("dark");
const [hudColor, setHudColor] = useState<HudColor>("cyan");
const [theme, setTheme] = useState<Theme>('dark');
const [hudColor, setHudColor] = useState<HudColor>('cyan');
useEffect(() => {
const saved = localStorage.getItem("sb-theme") as Theme | null;
if (saved === "light" || saved === "dark") {
const saved = localStorage.getItem('sb-theme') as Theme | null;
if (saved === 'light' || saved === 'dark') {
setTheme(saved);
document.documentElement.setAttribute("data-theme", saved);
document.documentElement.setAttribute('data-theme', saved);
}
const savedHud = localStorage.getItem("sb-hud-color") as HudColor | null;
if (savedHud === "cyan" || savedHud === "matrix") {
const savedHud = localStorage.getItem('sb-hud-color') as HudColor | null;
if (savedHud === 'cyan' || savedHud === 'matrix') {
setHudColor(savedHud);
document.documentElement.setAttribute("data-hud", savedHud);
document.documentElement.setAttribute('data-hud', savedHud);
}
}, []);
const toggleTheme = () => {
const next = theme === "dark" ? "light" : "dark";
const next = theme === 'dark' ? 'light' : 'dark';
setTheme(next);
localStorage.setItem("sb-theme", next);
document.documentElement.setAttribute("data-theme", next);
localStorage.setItem('sb-theme', next);
document.documentElement.setAttribute('data-theme', next);
};
const cycleHudColor = () => {
const next = hudColor === "cyan" ? "matrix" : "cyan";
const next = hudColor === 'cyan' ? 'matrix' : 'cyan';
setHudColor(next);
localStorage.setItem("sb-hud-color", next);
document.documentElement.setAttribute("data-hud", next);
localStorage.setItem('sb-hud-color', next);
document.documentElement.setAttribute('data-hud', next);
};
return (
+63
View File
@@ -0,0 +1,63 @@
import { API_BASE } from '@/lib/api';
let hasPrimedSessionHint = false;
function takeLegacyAdminKey(): string {
if (typeof window === 'undefined') return '';
const sessionValue = sessionStorage.getItem('sb_admin_key') || '';
const legacyValue = localStorage.getItem('sb_admin_key') || '';
const candidate = sessionValue || legacyValue;
try {
sessionStorage.removeItem('sb_admin_key');
localStorage.removeItem('sb_admin_key');
} catch {
/* ignore */
}
return candidate;
}
export async function hasAdminSession(): Promise<boolean> {
try {
const existing = await fetch(`${API_BASE}/api/admin/session`, { cache: 'no-store' });
const existingData = await existing.json().catch(() => ({}));
return Boolean(existing.ok && existingData?.hasSession);
} catch {
return false;
}
}
export async function primeAdminSession(adminKey?: string): Promise<void> {
if (!adminKey) {
if (await hasAdminSession()) return;
}
const candidate = String(adminKey || takeLegacyAdminKey() || '').trim();
if (!candidate) throw new Error('admin_session_required');
if (hasPrimedSessionHint && (await hasAdminSession())) return;
const res = await fetch(`${API_BASE}/api/admin/session`, {
method: 'POST',
cache: 'no-store',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ adminKey: candidate }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data?.ok === false) {
throw new Error(data?.detail || data?.message || 'admin_session_failed');
}
hasPrimedSessionHint = true;
}
export async function clearAdminSession(): Promise<void> {
hasPrimedSessionHint = false;
if (typeof window !== 'undefined') {
try {
sessionStorage.removeItem('sb_admin_key');
localStorage.removeItem('sb_admin_key');
} catch {
/* ignore */
}
}
await fetch(`${API_BASE}/api/admin/session`, {
method: 'DELETE',
cache: 'no-store',
}).catch(() => null);
}
File diff suppressed because it is too large Load Diff
+60650 -12129
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -5,4 +5,4 @@
// - No build-time baking of the backend URL into the client bundle
// - BACKEND_URL=http://backend:8000 works via Docker internal networking
// - Only port 3000 needs to be exposed externally
export const API_BASE = "";
export const API_BASE = '';
+3 -3
View File
@@ -8,13 +8,13 @@ export const POLL_SLOW_STARTUP_MS = 5000;
export const POLL_SLOW_STEADY_MS = 120000;
// ─── Reverse Geocoding ──────────────────────────────────────────────────────
export const GEOCODE_THROTTLE_MS = 1500;
export const GEOCODE_DISTANCE_THRESHOLD = 0.05; // ~5km in degrees
export const GEOCODE_THROTTLE_MS = 2200;
export const GEOCODE_DISTANCE_THRESHOLD = 0.12; // ~13km in degrees
export const GEOCODE_CACHE_SIZE = 500;
export const NOMINATIM_DEBOUNCE_MS = 350;
// ─── Map Interpolation ─────────────────────────────────────────────────────
export const INTERP_TICK_MS = 1000;
export const INTERP_TICK_MS = 2000;
// ─── News/Alert Layout ──────────────────────────────────────────────────────
export const ALERT_BOX_WIDTH_PX = 180;
+52
View File
@@ -0,0 +1,52 @@
import { primeAdminSession } from '@/lib/adminSession';
import type {
DesktopControlCapability,
DesktopControlSessionProfile,
} from '@/lib/desktopControlContract';
import {
canInvokeLocalControl,
hasLocalControlBridge,
localControlFetch,
} from '@/lib/localControlTransport';
type ControlPlaneOptions = RequestInit & {
requireAdminSession?: boolean;
capabilityIntent?: DesktopControlCapability;
sessionProfileHint?: DesktopControlSessionProfile;
enforceProfileHint?: boolean;
};
export async function controlPlaneFetch(
path: string,
options: ControlPlaneOptions = {},
): Promise<Response> {
const {
requireAdminSession = true,
capabilityIntent,
sessionProfileHint,
enforceProfileHint,
...init
} = options;
const nativePrivilegedPath = hasLocalControlBridge() && canInvokeLocalControl(path, init);
if (requireAdminSession && !nativePrivilegedPath) {
await primeAdminSession();
}
return localControlFetch(path, {
...init,
capabilityIntent,
sessionProfileHint,
enforceProfileHint,
});
}
export async function controlPlaneJson<T>(
path: string,
options: ControlPlaneOptions = {},
): Promise<T> {
const res = await controlPlaneFetch(path, options);
const data = await res.json().catch(() => ({}));
if (!res.ok || data?.ok === false) {
throw new Error(data?.detail || data?.message || 'control_plane_request_failed');
}
return data as T;
}
+55
View File
@@ -0,0 +1,55 @@
import { createHttpBackedDesktopRuntime } from '@/lib/desktopRuntimeShim';
import type {
DesktopControlAuditReport,
DesktopControlCommand,
LocalControlInvokeMeta,
LocalControlInvokeRequest,
} from '@/lib/desktopControlContract';
import type { ShadowbrokerLocalControlBridge } from '@/lib/localControlTransport';
export interface ShadowbrokerDesktopRuntime {
invokeLocalControl?<T = unknown>(
command: DesktopControlCommand,
payload?: unknown,
meta?: LocalControlInvokeMeta,
): Promise<T>;
getNativeControlAuditReport?(limit?: number): DesktopControlAuditReport;
clearNativeControlAuditReport?(): void;
}
function buildDesktopControlBridge(
runtime: ShadowbrokerDesktopRuntime,
): ShadowbrokerLocalControlBridge | null {
if (!runtime.invokeLocalControl) return null;
return {
invoke<T = unknown>(input: LocalControlInvokeRequest): Promise<T> {
return runtime.invokeLocalControl!(input.command, input.payload, input.meta);
},
};
}
export function installDesktopControlBridge(runtime: ShadowbrokerDesktopRuntime): boolean {
if (typeof window === 'undefined') return false;
const bridge = buildDesktopControlBridge(runtime);
if (!bridge) return false;
window.__SHADOWBROKER_LOCAL_CONTROL__ = bridge;
window.__SHADOWBROKER_DESKTOP__ = runtime;
return true;
}
export function bootstrapDesktopControlBridge(): boolean {
if (typeof window === 'undefined') return false;
const runtime =
window.__SHADOWBROKER_DESKTOP__ ||
(process.env.NEXT_PUBLIC_ENABLE_DESKTOP_BRIDGE_SHIM === '1'
? createHttpBackedDesktopRuntime()
: undefined);
if (!runtime) return false;
return installDesktopControlBridge(runtime);
}
export function getDesktopNativeControlAuditReport(limit?: number): DesktopControlAuditReport | null {
if (typeof window === 'undefined') return null;
const runtime = window.__SHADOWBROKER_DESKTOP__;
return runtime?.getNativeControlAuditReport?.(limit) || null;
}
+296
View File
@@ -0,0 +1,296 @@
export const DESKTOP_CONTROL_COMMANDS = [
'wormhole.status',
'wormhole.connect',
'wormhole.disconnect',
'wormhole.restart',
'wormhole.gate.enter',
'wormhole.gate.leave',
'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.proof',
'wormhole.gate.message.compose',
'wormhole.gate.message.post',
'wormhole.gate.message.decrypt',
'wormhole.gate.messages.decrypt',
'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',
'system.update',
] as const;
export type DesktopControlCommand = (typeof DESKTOP_CONTROL_COMMANDS)[number];
export type DesktopControlCapability =
| 'wormhole_gate_persona'
| 'wormhole_gate_key'
| 'wormhole_gate_content'
| 'wormhole_runtime'
| 'settings';
export type DesktopControlSessionProfile =
| 'full_app'
| 'gate_observe'
| 'gate_operator'
| 'wormhole_runtime'
| 'settings_only';
export type DesktopControlAuditOutcome =
| 'allowed'
| 'profile_warn'
| 'profile_denied'
| 'capability_denied'
| 'capability_mismatch'
| 'shim_refused';
export interface DesktopWormholeSettingsPayload {
enabled: boolean;
transport: string;
socks_proxy: string;
socks_dns: boolean;
anonymous_mode: boolean;
}
export interface DesktopGateRequestPayload {
gate_id: string;
rotate?: boolean;
}
export interface DesktopGatePersonaCreatePayload {
gate_id: string;
label: string;
}
export interface DesktopGatePersonaActivatePayload {
gate_id: string;
persona_id: string;
}
export interface DesktopGateRotatePayload {
gate_id: string;
reason: string;
}
export interface DesktopGateComposePayload {
gate_id: string;
plaintext: string;
}
export interface DesktopGateDecryptPayload {
gate_id: string;
epoch: number;
ciphertext: string;
nonce: string;
sender_ref: string;
}
export interface DesktopGateDecryptBatchPayload {
messages: DesktopGateDecryptPayload[];
}
export interface DesktopPrivacySettingsPayload {
profile: string;
}
export interface DesktopApiKeyPayload {
env_key: string;
value: string;
}
export interface DesktopNewsFeedPayload {
name: string;
url: string;
weight: number;
}
export interface DesktopControlPayloadMap {
'wormhole.status': undefined;
'wormhole.connect': undefined;
'wormhole.disconnect': undefined;
'wormhole.restart': undefined;
'wormhole.gate.enter': DesktopGateRequestPayload;
'wormhole.gate.leave': DesktopGateRequestPayload;
'wormhole.gate.personas.get': DesktopGateRequestPayload;
'wormhole.gate.persona.create': DesktopGatePersonaCreatePayload;
'wormhole.gate.persona.activate': DesktopGatePersonaActivatePayload;
'wormhole.gate.persona.clear': DesktopGateRequestPayload;
'wormhole.gate.key.get': DesktopGateRequestPayload;
'wormhole.gate.key.rotate': DesktopGateRotatePayload;
'wormhole.gate.proof': DesktopGateRequestPayload;
'wormhole.gate.message.compose': DesktopGateComposePayload;
'wormhole.gate.message.post': DesktopGateComposePayload;
'wormhole.gate.message.decrypt': DesktopGateDecryptPayload;
'wormhole.gate.messages.decrypt': DesktopGateDecryptBatchPayload;
'settings.wormhole.get': undefined;
'settings.wormhole.set': DesktopWormholeSettingsPayload;
'settings.privacy.get': undefined;
'settings.privacy.set': DesktopPrivacySettingsPayload;
'settings.api_keys.get': undefined;
'settings.api_keys.set': DesktopApiKeyPayload;
'settings.news.get': undefined;
'settings.news.set': DesktopNewsFeedPayload[];
'settings.news.reset': undefined;
'system.update': undefined;
}
export type DesktopControlResponseMap = {
[K in DesktopControlCommand]: unknown;
};
export function controlCommandCapability(
command: DesktopControlCommand,
): DesktopControlCapability {
switch (command) {
case 'wormhole.status':
case 'wormhole.connect':
case 'wormhole.disconnect':
case 'wormhole.restart':
return 'wormhole_runtime';
case 'wormhole.gate.enter':
case 'wormhole.gate.leave':
case 'wormhole.gate.personas.get':
case 'wormhole.gate.persona.create':
case 'wormhole.gate.persona.activate':
case 'wormhole.gate.persona.clear':
return 'wormhole_gate_persona';
case 'wormhole.gate.key.get':
case 'wormhole.gate.key.rotate':
return 'wormhole_gate_key';
case 'wormhole.gate.proof':
case 'wormhole.gate.message.compose':
case 'wormhole.gate.message.post':
case 'wormhole.gate.message.decrypt':
case 'wormhole.gate.messages.decrypt':
return 'wormhole_gate_content';
case 'settings.wormhole.get':
case 'settings.wormhole.set':
case 'settings.privacy.get':
case 'settings.privacy.set':
case 'settings.api_keys.get':
case 'settings.api_keys.set':
case 'settings.news.get':
case 'settings.news.set':
case 'settings.news.reset':
case 'system.update':
return 'settings';
}
}
export function sessionProfileCapabilities(
profile: DesktopControlSessionProfile,
): DesktopControlCapability[] {
switch (profile) {
case 'full_app':
return [
'wormhole_gate_persona',
'wormhole_gate_key',
'wormhole_gate_content',
'wormhole_runtime',
'settings',
];
case 'gate_observe':
return ['wormhole_gate_content'];
case 'gate_operator':
return ['wormhole_gate_persona', 'wormhole_gate_key', 'wormhole_gate_content'];
case 'wormhole_runtime':
return ['wormhole_runtime'];
case 'settings_only':
return ['settings'];
}
}
export type LocalControlInvokeMeta = {
capability?: DesktopControlCapability;
sessionProfileHint?: DesktopControlSessionProfile;
enforceProfileHint?: boolean;
};
export type DesktopControlAuditEvent = {
command: DesktopControlCommand;
expectedCapability: DesktopControlCapability;
declaredCapability?: DesktopControlCapability;
targetRef?: string;
sessionProfile?: DesktopControlSessionProfile;
sessionProfileHint?: DesktopControlSessionProfile;
enforceProfileHint?: boolean;
profileAllows: boolean;
allowedCapabilitiesConfigured: boolean;
enforced: boolean;
outcome: DesktopControlAuditOutcome;
};
export type DesktopControlAuditRecord = DesktopControlAuditEvent & {
recordedAt: number;
};
export type DesktopControlAuditReport = {
totalEvents: number;
totalRecorded: number;
recent: DesktopControlAuditRecord[];
byOutcome: Partial<Record<DesktopControlAuditOutcome, number>>;
lastProfileMismatch?: DesktopControlAuditRecord;
lastDenied?: DesktopControlAuditRecord;
};
export type LocalControlInvokeRequest<C extends DesktopControlCommand = DesktopControlCommand> =
DesktopControlPayloadMap[C] extends undefined
? { command: C; payload?: undefined; meta?: LocalControlInvokeMeta }
: { command: C; payload: DesktopControlPayloadMap[C]; meta?: LocalControlInvokeMeta };
export function isDesktopControlCommand(value: string): value is DesktopControlCommand {
return (DESKTOP_CONTROL_COMMANDS as readonly string[]).includes(value);
}
export function describeNativeControlError(err: unknown): string | null {
const msg =
typeof err === 'object' && err !== null && 'message' in err
? String((err as { message?: string }).message || '')
: typeof err === 'string'
? err
: '';
if (msg.includes('native_control_profile_mismatch')) {
return 'Denied — current native session profile does not include the required access';
}
if (msg.includes('native_control_capability_denied')) {
return 'Denied — this capability is not in the allowed set for this native session';
}
if (msg.includes('native_control_capability_mismatch')) {
return 'Denied — declared capability does not match the command being invoked';
}
if (msg.includes('desktop_runtime_shim_enforcement_inactive')) {
return 'Denied — this command requires a native runtime with session-profile enforcement';
}
return null;
}
export function extractGateTargetRef(
command: DesktopControlCommand,
payload: unknown,
): string | undefined {
if (!payload || typeof payload !== 'object') return undefined;
const gateId = (payload as Record<string, unknown>).gate_id;
if (typeof gateId !== 'string' || !gateId) return undefined;
switch (command) {
case 'wormhole.gate.enter':
case 'wormhole.gate.leave':
case 'wormhole.gate.personas.get':
case 'wormhole.gate.persona.create':
case 'wormhole.gate.persona.activate':
case 'wormhole.gate.persona.clear':
case 'wormhole.gate.key.get':
case 'wormhole.gate.key.rotate':
case 'wormhole.gate.proof':
case 'wormhole.gate.message.compose':
case 'wormhole.gate.message.post':
case 'wormhole.gate.message.decrypt':
return gateId;
default:
return undefined;
}
}
+257
View File
@@ -0,0 +1,257 @@
import type {
DesktopApiKeyPayload,
DesktopControlCommand,
DesktopGateComposePayload,
DesktopGateDecryptBatchPayload,
DesktopGateDecryptPayload,
DesktopGatePersonaActivatePayload,
DesktopGatePersonaCreatePayload,
DesktopGateRequestPayload,
DesktopGateRotatePayload,
DesktopNewsFeedPayload,
DesktopPrivacySettingsPayload,
DesktopWormholeSettingsPayload,
LocalControlInvokeRequest,
} from '@/lib/desktopControlContract';
export type DesktopControlHttpRequest = {
path: string;
method: string;
payload?:
| DesktopWormholeSettingsPayload
| DesktopPrivacySettingsPayload
| DesktopApiKeyPayload
| DesktopNewsFeedPayload[]
| DesktopGateRequestPayload
| DesktopGatePersonaCreatePayload
| DesktopGatePersonaActivatePayload
| DesktopGateRotatePayload
| DesktopGateComposePayload
| DesktopGateDecryptPayload
| DesktopGateDecryptBatchPayload;
};
function parseJsonBody(bodyText?: string): unknown {
if (!bodyText) return undefined;
try {
return JSON.parse(bodyText);
} catch {
return undefined;
}
}
export function commandToHttpRequest(
command: DesktopControlCommand,
payload?: unknown,
): DesktopControlHttpRequest {
switch (command) {
case 'wormhole.status':
return { path: '/api/wormhole/status', method: 'GET' };
case 'wormhole.connect':
return { path: '/api/wormhole/connect', method: 'POST' };
case 'wormhole.disconnect':
return { path: '/api/wormhole/disconnect', method: 'POST' };
case 'wormhole.restart':
return { path: '/api/wormhole/restart', method: 'POST' };
case 'wormhole.gate.enter':
return {
path: '/api/wormhole/gate/enter',
method: 'POST',
payload: payload as DesktopGateRequestPayload,
};
case 'wormhole.gate.leave':
return {
path: '/api/wormhole/gate/leave',
method: 'POST',
payload: payload as DesktopGateRequestPayload,
};
case 'wormhole.gate.personas.get':
return {
path: `/api/wormhole/gate/${encodeURIComponent((payload as DesktopGateRequestPayload).gate_id)}/personas`,
method: 'GET',
};
case 'wormhole.gate.persona.create':
return {
path: '/api/wormhole/gate/persona/create',
method: 'POST',
payload: payload as DesktopGatePersonaCreatePayload,
};
case 'wormhole.gate.persona.activate':
return {
path: '/api/wormhole/gate/persona/activate',
method: 'POST',
payload: payload as DesktopGatePersonaActivatePayload,
};
case 'wormhole.gate.persona.clear':
return {
path: '/api/wormhole/gate/persona/clear',
method: 'POST',
payload: payload as DesktopGateRequestPayload,
};
case 'wormhole.gate.key.get':
return {
path: `/api/wormhole/gate/${encodeURIComponent((payload as DesktopGateRequestPayload).gate_id)}/key`,
method: 'GET',
};
case 'wormhole.gate.key.rotate':
return {
path: '/api/wormhole/gate/key/rotate',
method: 'POST',
payload: payload as DesktopGateRotatePayload,
};
case 'wormhole.gate.proof':
return {
path: '/api/wormhole/gate/proof',
method: 'POST',
payload: payload as DesktopGateRequestPayload,
};
case 'wormhole.gate.message.compose':
return {
path: '/api/wormhole/gate/message/compose',
method: 'POST',
payload: payload as DesktopGateComposePayload,
};
case 'wormhole.gate.message.post':
return {
path: '/api/wormhole/gate/message/post',
method: 'POST',
payload: payload as DesktopGateComposePayload,
};
case 'wormhole.gate.message.decrypt':
return {
path: '/api/wormhole/gate/message/decrypt',
method: 'POST',
payload: payload as DesktopGateDecryptPayload,
};
case 'wormhole.gate.messages.decrypt':
return {
path: '/api/wormhole/gate/messages/decrypt',
method: 'POST',
payload: payload as DesktopGateDecryptBatchPayload,
};
case 'settings.wormhole.get':
return { path: '/api/settings/wormhole', method: 'GET' };
case 'settings.wormhole.set':
return { path: '/api/settings/wormhole', method: 'PUT', payload: payload as DesktopWormholeSettingsPayload };
case 'settings.privacy.get':
return { path: '/api/settings/privacy-profile', method: 'GET' };
case 'settings.privacy.set':
return {
path: '/api/settings/privacy-profile',
method: 'PUT',
payload: payload as DesktopPrivacySettingsPayload,
};
case 'settings.api_keys.get':
return { path: '/api/settings/api-keys', method: 'GET' };
case 'settings.api_keys.set':
return { path: '/api/settings/api-keys', method: 'PUT', payload: payload as DesktopApiKeyPayload };
case 'settings.news.get':
return { path: '/api/settings/news-feeds', method: 'GET' };
case 'settings.news.set':
return { path: '/api/settings/news-feeds', method: 'PUT', payload: payload as DesktopNewsFeedPayload[] };
case 'settings.news.reset':
return { path: '/api/settings/news-feeds/reset', method: 'POST' };
case 'system.update':
return { path: '/api/system/update', method: 'POST' };
default: {
const exhaustive: never = command;
throw new Error(`desktop_control_command_unsupported:${exhaustive}`);
}
}
}
export function httpRequestToInvokeRequest(
path: string,
method: string,
bodyText?: string,
): LocalControlInvokeRequest | null {
const payload = parseJsonBody(bodyText);
const upperMethod = method.toUpperCase();
if (upperMethod === 'GET' && path === '/api/wormhole/status') {
return { command: 'wormhole.status', payload: undefined };
}
if (upperMethod === 'POST' && path === '/api/wormhole/connect') {
return { command: 'wormhole.connect', payload: undefined };
}
if (upperMethod === 'POST' && path === '/api/wormhole/disconnect') {
return { command: 'wormhole.disconnect', payload: undefined };
}
if (upperMethod === 'POST' && path === '/api/wormhole/restart') {
return { command: 'wormhole.restart', payload: undefined };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/enter') {
return { command: 'wormhole.gate.enter', payload: payload as DesktopGateRequestPayload };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/leave') {
return { command: 'wormhole.gate.leave', payload: payload as DesktopGateRequestPayload };
}
if (upperMethod === 'GET' && /^\/api\/wormhole\/gate\/[^/]+\/personas$/.test(path)) {
const gateId = decodeURIComponent(path.split('/')[4] || '');
return { command: 'wormhole.gate.personas.get', payload: { gate_id: gateId } };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/persona/create') {
return { command: 'wormhole.gate.persona.create', payload: payload as DesktopGatePersonaCreatePayload };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/persona/activate') {
return { command: 'wormhole.gate.persona.activate', payload: payload as DesktopGatePersonaActivatePayload };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/persona/clear') {
return { command: 'wormhole.gate.persona.clear', payload: payload as DesktopGateRequestPayload };
}
if (upperMethod === 'GET' && /^\/api\/wormhole\/gate\/[^/]+\/key$/.test(path)) {
const gateId = decodeURIComponent(path.split('/')[4] || '');
return { command: 'wormhole.gate.key.get', payload: { gate_id: gateId } };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/key/rotate') {
return { command: 'wormhole.gate.key.rotate', payload: payload as DesktopGateRotatePayload };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/proof') {
return { command: 'wormhole.gate.proof', payload: payload as DesktopGateRequestPayload };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/message/compose') {
return { command: 'wormhole.gate.message.compose', payload: payload as DesktopGateComposePayload };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/message/post') {
return { command: 'wormhole.gate.message.post', payload: payload as DesktopGateComposePayload };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/message/decrypt') {
return { command: 'wormhole.gate.message.decrypt', payload: payload as DesktopGateDecryptPayload };
}
if (upperMethod === 'POST' && path === '/api/wormhole/gate/messages/decrypt') {
return {
command: 'wormhole.gate.messages.decrypt',
payload: payload as DesktopGateDecryptBatchPayload,
};
}
if (upperMethod === 'GET' && path === '/api/settings/wormhole') {
return { command: 'settings.wormhole.get', payload: undefined };
}
if (upperMethod === 'PUT' && path === '/api/settings/wormhole') {
return { command: 'settings.wormhole.set', payload: payload as DesktopWormholeSettingsPayload };
}
if (upperMethod === 'GET' && path === '/api/settings/privacy-profile') {
return { command: 'settings.privacy.get', payload: undefined };
}
if (upperMethod === 'PUT' && path === '/api/settings/privacy-profile') {
return { command: 'settings.privacy.set', payload: payload as DesktopPrivacySettingsPayload };
}
if (upperMethod === 'GET' && path === '/api/settings/api-keys') {
return { command: 'settings.api_keys.get', payload: undefined };
}
if (upperMethod === 'PUT' && path === '/api/settings/api-keys') {
return { command: 'settings.api_keys.set', payload: payload as DesktopApiKeyPayload };
}
if (upperMethod === 'GET' && path === '/api/settings/news-feeds') {
return { command: 'settings.news.get', payload: undefined };
}
if (upperMethod === 'PUT' && path === '/api/settings/news-feeds') {
return { command: 'settings.news.set', payload: payload as DesktopNewsFeedPayload[] };
}
if (upperMethod === 'POST' && path === '/api/settings/news-feeds/reset') {
return { command: 'settings.news.reset', payload: undefined };
}
if (upperMethod === 'POST' && path === '/api/system/update') {
return { command: 'system.update', payload: undefined };
}
return null;
}
+86
View File
@@ -0,0 +1,86 @@
import { API_BASE } from '@/lib/api';
import {
controlCommandCapability,
extractGateTargetRef,
type DesktopControlAuditRecord,
type DesktopControlAuditReport,
type DesktopControlCommand,
type LocalControlInvokeMeta,
} from '@/lib/desktopControlContract';
import { commandToHttpRequest } from '@/lib/desktopControlRouting';
import type { ShadowbrokerDesktopRuntime } from '@/lib/desktopBridge';
export function createHttpBackedDesktopRuntime(): ShadowbrokerDesktopRuntime {
const auditEntries: DesktopControlAuditRecord[] = [];
let totalRecorded = 0;
const recordAudit = (entry: Omit<DesktopControlAuditRecord, 'recordedAt'>) => {
totalRecorded += 1;
auditEntries.push({ ...entry, recordedAt: Date.now() });
if (auditEntries.length > 50) {
auditEntries.splice(0, auditEntries.length - 50);
}
};
const snapshotAudit = (limit: number = 10): DesktopControlAuditReport => {
const recent = auditEntries.slice(-Math.max(1, limit)).reverse();
const byOutcome: DesktopControlAuditReport['byOutcome'] = {};
let lastDenied: DesktopControlAuditRecord | undefined;
for (const entry of auditEntries) {
byOutcome[entry.outcome] = (byOutcome[entry.outcome] || 0) + 1;
if (entry.outcome === 'shim_refused' || entry.outcome === 'profile_denied') {
lastDenied = entry;
}
}
return {
totalEvents: auditEntries.length,
totalRecorded,
recent,
byOutcome,
lastDenied,
};
};
return {
async invokeLocalControl<T = unknown>(
command: DesktopControlCommand,
payload?: unknown,
meta?: LocalControlInvokeMeta,
): Promise<T> {
if (meta?.enforceProfileHint) {
recordAudit({
command,
expectedCapability: controlCommandCapability(command),
declaredCapability: meta.capability,
targetRef: extractGateTargetRef(command, payload),
sessionProfileHint: meta.sessionProfileHint,
enforceProfileHint: true,
profileAllows: false,
allowedCapabilitiesConfigured: false,
enforced: true,
outcome: 'shim_refused',
});
console.warn(
'[desktop-shim] strict native session-profile enforcement is unavailable in the HTTP-backed shim',
{ command, sessionProfileHint: meta.sessionProfileHint },
);
throw new Error('desktop_runtime_shim_enforcement_inactive');
}
const request = commandToHttpRequest(command, payload);
const res = await fetch(`${API_BASE}${request.path}`, {
method: request.method,
headers: request.payload ? { 'Content-Type': 'application/json' } : undefined,
body: request.payload ? JSON.stringify(request.payload) : undefined,
});
const data = await res.json().catch(() => ({}));
if (!res.ok || data?.ok === false) {
throw new Error(data?.detail || data?.message || 'desktop_shim_request_failed');
}
return data as T;
},
getNativeControlAuditReport(limit?: number) {
return snapshotAudit(limit);
},
clearNativeControlAuditReport() {
totalRecorded = 0;
auditEntries.splice(0, auditEntries.length);
},
};
}
@@ -0,0 +1,46 @@
import {
decryptIdentityBoundStoragePayload,
encryptIdentityBoundStoragePayload,
} from '@/mesh/meshIdentity';
import {
getSensitiveBrowserItem,
removeSensitiveBrowserItem,
setSensitiveBrowserItem,
} from '@/lib/privacyBrowserStorage';
type SensitiveStorageOptions = {
legacyKey?: string;
};
export async function loadIdentityBoundSensitiveValue<T>(
storageKey: string,
wrapInfo: string,
fallback: T,
options: SensitiveStorageOptions = {},
): Promise<T> {
const scopedRaw = getSensitiveBrowserItem(storageKey) || '';
const legacyKey = String(options.legacyKey || '').trim();
const legacyRaw = legacyKey ? getSensitiveBrowserItem(legacyKey) || '' : '';
const raw = scopedRaw || legacyRaw;
if (!raw) return fallback;
const value = await decryptIdentityBoundStoragePayload<T>(raw, wrapInfo, fallback);
if (!raw.trim().startsWith('enc:')) {
await persistIdentityBoundSensitiveValue(storageKey, wrapInfo, value, options);
}
return value;
}
export async function persistIdentityBoundSensitiveValue<T>(
storageKey: string,
wrapInfo: string,
value: T,
options: SensitiveStorageOptions = {},
): Promise<void> {
const encrypted = await encryptIdentityBoundStoragePayload(value, wrapInfo);
setSensitiveBrowserItem(storageKey, encrypted);
const legacyKey = String(options.legacyKey || '').trim();
if (legacyKey && legacyKey !== storageKey) {
removeSensitiveBrowserItem(legacyKey);
}
}
+118
View File
@@ -0,0 +1,118 @@
import { API_BASE } from '@/lib/api';
import type {
DesktopControlCapability,
DesktopControlSessionProfile,
LocalControlInvokeRequest,
} from '@/lib/desktopControlContract';
import { httpRequestToInvokeRequest } from '@/lib/desktopControlRouting';
export interface LocalControlBridgeResponse {
status: number;
headers?: Record<string, string>;
bodyText?: string;
}
export interface LocalControlBridgeRequest {
path: string;
method: string;
headers: Record<string, string>;
bodyText?: string;
}
export type LocalControlFetchOptions = RequestInit & {
capabilityIntent?: DesktopControlCapability;
sessionProfileHint?: DesktopControlSessionProfile;
enforceProfileHint?: boolean;
};
export interface ShadowbrokerLocalControlBridge {
request?(input: LocalControlBridgeRequest): Promise<LocalControlBridgeResponse>;
invoke?<T = unknown>(input: LocalControlInvokeRequest): Promise<T>;
}
function getDesktopBridge(): ShadowbrokerLocalControlBridge | null {
if (typeof window === 'undefined') return null;
return window.__SHADOWBROKER_LOCAL_CONTROL__ || null;
}
export function hasLocalControlBridge(): boolean {
return Boolean(getDesktopBridge());
}
export function canInvokeLocalControl(path: string, init: RequestInit = {}): boolean {
const bridge = getDesktopBridge();
if (!bridge?.invoke) return false;
return Boolean(
httpRequestToInvokeRequest(path, String(init.method || 'GET'), normalizeBody(init.body)),
);
}
function normalizeHeaders(headers?: HeadersInit): Record<string, string> {
const normalized = new Headers(headers);
const result: Record<string, string> = {};
normalized.forEach((value, key) => {
result[key] = value;
});
return result;
}
function normalizeBody(body: BodyInit | null | undefined): string | undefined {
if (body == null) return undefined;
if (typeof body === 'string') return body;
if (body instanceof URLSearchParams) return body.toString();
return undefined;
}
export async function localControlFetch(
path: string,
init: LocalControlFetchOptions = {},
): Promise<Response> {
const bridge = getDesktopBridge();
if (!bridge) {
return fetch(`${API_BASE}${path}`, init);
}
const { capabilityIntent, sessionProfileHint, enforceProfileHint, ...requestInit } = init;
const invokeRequest = bridge.invoke
? httpRequestToInvokeRequest(
path,
String(requestInit.method || 'GET'),
normalizeBody(requestInit.body),
)
: null;
if (bridge.invoke && invokeRequest) {
try {
const data = await bridge.invoke({
...invokeRequest,
meta: {
...invokeRequest.meta,
...(capabilityIntent ? { capability: capabilityIntent } : {}),
...(sessionProfileHint ? { sessionProfileHint } : {}),
...(enforceProfileHint ? { enforceProfileHint: true } : {}),
},
} as LocalControlInvokeRequest);
return new Response(JSON.stringify(data ?? {}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
const message =
error instanceof Error ? error.message : typeof error === 'string' ? error : '';
if (!message.includes('desktop_runtime_shim_enforcement_inactive')) {
throw error;
}
}
}
if (!bridge.request) {
return fetch(`${API_BASE}${path}`, requestInit);
}
const response = await bridge.request({
path,
method: requestInit.method || 'GET',
headers: normalizeHeaders(requestInit.headers),
bodyText: normalizeBody(requestInit.body),
});
return new Response(response.bodyText || '', {
status: response.status,
headers: response.headers,
});
}
+40
View File
@@ -0,0 +1,40 @@
const MESH_TERMINAL_OPEN_EVENT = 'oracle:open-mesh-terminal';
const SECURE_MESH_TERMINAL_LAUNCHER_EVENT = 'oracle:open-secure-mesh-terminal-launcher';
export function requestMeshTerminalOpen(source = 'ui'): void {
if (typeof window === 'undefined') return;
window.dispatchEvent(
new CustomEvent(MESH_TERMINAL_OPEN_EVENT, {
detail: { source, at: Date.now() },
}),
);
}
export function subscribeMeshTerminalOpen(handler: () => void): () => void {
if (typeof window === 'undefined') {
return () => {};
}
const listener = () => handler();
window.addEventListener(MESH_TERMINAL_OPEN_EVENT, listener);
return () => window.removeEventListener(MESH_TERMINAL_OPEN_EVENT, listener);
}
export function requestSecureMeshTerminalLauncherOpen(source = 'ui'): void {
if (typeof window === 'undefined') return;
window.dispatchEvent(
new CustomEvent(SECURE_MESH_TERMINAL_LAUNCHER_EVENT, {
detail: { source, at: Date.now() },
}),
);
}
export function subscribeSecureMeshTerminalLauncherOpen(handler: () => void): () => void {
if (typeof window === 'undefined') {
return () => {};
}
const listener = () => handler();
window.addEventListener(SECURE_MESH_TERMINAL_LAUNCHER_EVENT, listener);
return () => window.removeEventListener(SECURE_MESH_TERMINAL_LAUNCHER_EVENT, listener);
}
+58
View File
@@ -0,0 +1,58 @@
export type MeshTerminalSecurityState = {
wormholeRequired: boolean;
wormholeReady: boolean;
anonymousMode: boolean;
anonymousModeReady: boolean;
};
export function getMeshTerminalWriteLockReason(state: MeshTerminalSecurityState): string {
if (state.anonymousMode) {
if (!state.anonymousModeReady) {
return 'Mesh Terminal write commands are disabled until Wormhole hidden transport is ready for Anonymous Infonet mode.';
}
return 'Mesh Terminal write commands are disabled while Anonymous Infonet mode is active. Use MeshChat for hardened public and private actions.';
}
if (state.wormholeRequired) {
if (!state.wormholeReady) {
return 'Mesh Terminal write commands are disabled until Wormhole secure mode is ready.';
}
return 'Mesh Terminal write commands are disabled while Wormhole secure mode is active. Use MeshChat for hardened private actions.';
}
return '';
}
export function isMeshTerminalWriteCommand(cmd: string, args: string[]): boolean {
const command = String(cmd || '').trim().toLowerCase();
const sub = String(args[0] || '').trim().toLowerCase();
if (
[
'connect',
'sovereignty',
'sovereign',
'activate',
'join',
'send',
'vote',
'say',
'predict',
'stake',
'rotate',
'revoke',
'dm',
'inbox',
].includes(command)
) {
return true;
}
if (command === 'mesh' || command === 'radio') {
return sub === 'send' || sub === 's';
}
if (command === 'gate') {
return sub === 'create';
}
return false;
}
+146
View File
@@ -0,0 +1,146 @@
const PRIVACY_STRICT_KEY = 'sb_privacy_strict';
const PRIVACY_PROFILE_KEY = 'sb_privacy_profile';
const SESSION_MODE_KEY = 'sb_mesh_session_mode';
type BrowserStorageMode = 'local' | 'session';
function browserAvailable(): boolean {
return typeof window !== 'undefined';
}
function safeGet(store: Storage, key: string): string | null {
try {
return store.getItem(key);
} catch {
return null;
}
}
function safeSet(store: Storage, key: string, value: string): void {
try {
store.setItem(key, value);
} catch {
/* ignore */
}
}
function safeRemove(store: Storage, key: string): void {
try {
store.removeItem(key);
} catch {
/* ignore */
}
}
function readPreference(key: string): string | null {
if (!browserAvailable()) return null;
const sessionValue = safeGet(sessionStorage, key);
if (sessionValue !== null) return sessionValue;
return safeGet(localStorage, key);
}
function writePreference(key: string, value: string, mode: BrowserStorageMode): void {
if (!browserAvailable()) return;
const preferred = mode === 'session' ? sessionStorage : localStorage;
const alternate = mode === 'session' ? localStorage : sessionStorage;
safeSet(preferred, key, value);
safeRemove(alternate, key);
}
export function getSessionModePreference(): boolean {
return readPreference(SESSION_MODE_KEY) !== 'false';
}
export function setSessionModePreference(enabled: boolean): void {
writePreference(SESSION_MODE_KEY, enabled ? 'true' : 'false', enabled ? 'session' : 'local');
}
export function getPrivacyStrictPreference(): boolean {
return readPreference(PRIVACY_STRICT_KEY) === 'true';
}
export function setPrivacyStrictPreference(
enabled: boolean,
opts?: { sessionMode?: boolean },
): void {
const sessionMode = opts?.sessionMode ?? getSessionModePreference();
writePreference(
PRIVACY_STRICT_KEY,
enabled ? 'true' : 'false',
enabled || sessionMode ? 'session' : 'local',
);
}
export function getPrivacyProfilePreference(): string {
return readPreference(PRIVACY_PROFILE_KEY) || 'default';
}
export function setPrivacyProfilePreference(
profile: string,
opts?: { sessionMode?: boolean },
): void {
const normalized = String(profile || 'default') || 'default';
const sessionMode = opts?.sessionMode ?? getSessionModePreference();
writePreference(
PRIVACY_PROFILE_KEY,
normalized,
normalized === 'high' || sessionMode ? 'session' : 'local',
);
}
export function getSensitiveBrowserStorageMode(): BrowserStorageMode {
if (!browserAvailable()) return 'session';
const strict = getPrivacyStrictPreference();
const sessionMode = getSessionModePreference();
return strict || sessionMode ? 'session' : 'local';
}
function preferredSensitiveStorage(): Storage | null {
if (!browserAvailable()) return null;
return getSensitiveBrowserStorageMode() === 'session' ? sessionStorage : localStorage;
}
function alternateStorage(preferred: Storage): Storage | null {
if (!browserAvailable()) return null;
return preferred === localStorage ? sessionStorage : localStorage;
}
export function getSensitiveBrowserItem(key: string): string | null {
const preferred = preferredSensitiveStorage();
if (!preferred) return null;
const alternate = alternateStorage(preferred);
const preferredValue = safeGet(preferred, key);
if (preferredValue !== null) return preferredValue;
if (!alternate) return null;
const alternateValue = safeGet(alternate, key);
if (alternateValue !== null) {
safeSet(preferred, key, alternateValue);
if (alternate !== preferred) {
safeRemove(alternate, key);
}
}
return alternateValue;
}
export function setSensitiveBrowserItem(key: string, value: string): void {
const preferred = preferredSensitiveStorage();
if (!preferred) return;
const alternate = alternateStorage(preferred);
safeSet(preferred, key, value);
if (alternate && alternate !== preferred) {
safeRemove(alternate, key);
}
}
export function removeSensitiveBrowserItem(key: string): void {
if (!browserAvailable()) return;
safeRemove(localStorage, key);
safeRemove(sessionStorage, key);
}
export function migrateSensitiveBrowserItems(keys: string[]): void {
if (!browserAvailable()) return;
keys.forEach((key) => {
void getSensitiveBrowserItem(key);
});
}
+322
View File
@@ -0,0 +1,322 @@
/**
* Sentinel Hub (Copernicus CDSE) — client-side token management & Process API tile fetcher.
*
* Credentials are stored in browser-controlled storage only. In privacy/session
* mode they stay session-scoped; otherwise they persist in local storage. Token
* exchange is proxied through the ShadowBroker backend (/api/sentinel/token) to
* avoid CORS blocks from the Copernicus identity provider. Credentials are
* forwarded, never stored server-side.
*
* Uses the Process API with inline evalscripts — no Instance ID / Configuration needed.
*/
import { API_BASE } from '@/lib/api';
import {
getSensitiveBrowserItem,
getSensitiveBrowserStorageMode,
removeSensitiveBrowserItem,
setSensitiveBrowserItem,
} from '@/lib/privacyBrowserStorage';
// Token exchange proxied through our backend (Copernicus blocks browser CORS)
const TOKEN_PROXY_URL = `${API_BASE}/api/sentinel/token`;
// browser-storage keys
const LS_CLIENT_ID = 'sb_sentinel_client_id';
const LS_CLIENT_SECRET = 'sb_sentinel_client_secret';
// In-memory token cache (never persisted)
let cachedToken: string | null = null;
let tokenExpiry = 0;
// Dedup: only one in-flight token request at a time
let _tokenPromise: Promise<string | null> | null = null;
// ─── Credential helpers ────────────────────────────────────────────────────
export function getSentinelCredentials(): {
clientId: string;
clientSecret: string;
} {
if (typeof window === 'undefined') return { clientId: '', clientSecret: '' };
return {
clientId: getSensitiveBrowserItem(LS_CLIENT_ID) || '',
clientSecret: getSensitiveBrowserItem(LS_CLIENT_SECRET) || '',
};
}
export function setSentinelCredentials(clientId: string, clientSecret: string): void {
setSensitiveBrowserItem(LS_CLIENT_ID, clientId);
setSensitiveBrowserItem(LS_CLIENT_SECRET, clientSecret);
// Invalidate cached token when credentials change
cachedToken = null;
tokenExpiry = 0;
}
export function clearSentinelCredentials(): void {
removeSensitiveBrowserItem(LS_CLIENT_ID);
removeSensitiveBrowserItem(LS_CLIENT_SECRET);
// Also remove legacy instance ID if present
removeSensitiveBrowserItem('sb_sentinel_instance_id');
if (typeof window !== 'undefined') {
localStorage.removeItem('sb_sentinel_instance_id');
sessionStorage.removeItem('sb_sentinel_instance_id');
}
cachedToken = null;
tokenExpiry = 0;
}
export function getSentinelCredentialStorageMode(): 'local' | 'session' {
return getSensitiveBrowserStorageMode();
}
export function hasSentinelCredentials(): boolean {
const { clientId, clientSecret } = getSentinelCredentials();
return Boolean(clientId && clientSecret);
}
// ─── OAuth2 token ──────────────────────────────────────────────────────────
/**
* Fetch an OAuth2 access token using the client_credentials grant.
* Caches in memory; auto-refreshes 30 s before expiry.
*/
export function getSentinelToken(): Promise<string | null> {
// Return cached token if still valid (with 30 s margin)
if (cachedToken && Date.now() < tokenExpiry - 30_000) return Promise.resolve(cachedToken);
const { clientId, clientSecret } = getSentinelCredentials();
if (!clientId || !clientSecret) return Promise.resolve(null);
// Dedup: reuse in-flight request so 20 tiles don't each trigger a token fetch
if (_tokenPromise) return _tokenPromise;
_tokenPromise = (async () => {
try {
const resp = await fetch(TOKEN_PROXY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
}),
});
if (!resp.ok) {
const text = await resp.text().catch(() => '');
throw new Error(`Sentinel Hub token request failed (${resp.status}): ${text}`);
}
const data = await resp.json();
cachedToken = data.access_token;
tokenExpiry = Date.now() + (data.expires_in ?? 300) * 1000;
return cachedToken;
} finally {
_tokenPromise = null;
}
})();
return _tokenPromise;
}
/** Synchronous getter — returns the current cached token or null. */
export function getCachedSentinelToken(): string | null {
if (cachedToken && Date.now() < tokenExpiry - 5_000) return cachedToken;
return null;
}
// ─── Tile fetcher (proxied through backend) ───────────────────────────────
const TILE_PROXY_URL = `${API_BASE}/api/sentinel/tile`;
/**
* Fetch a single 256×256 tile via backend proxy to Sentinel Hub Process API.
* Returns a PNG ArrayBuffer or null on failure.
*/
export async function fetchSentinelTile(
z: number,
x: number,
y: number,
preset: string,
date: string,
): Promise<ArrayBuffer | null> {
const { clientId, clientSecret } = getSentinelCredentials();
if (!clientId || !clientSecret) return null;
const resp = await fetch(TILE_PROXY_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
preset,
date,
z,
x,
y,
}),
});
if (!resp.ok) return null;
return resp.arrayBuffer();
}
// ─── MapLibre protocol registration ───────────────────────────────────────
let _protocolRegistered = false;
/**
* Register the `sentinel://` custom protocol with MapLibre.
* Tile URLs look like: sentinel://z/x/y?preset=TRUE-COLOR&date=2024-06-01
*
* Call once at app startup or before adding the Sentinel source.
*/
export function registerSentinelProtocol(maplibregl: {
addProtocol: (
name: string,
handler: (
params: { url: string },
abortController: AbortController,
) => Promise<{ data: ArrayBuffer }>,
) => void;
}): void {
if (_protocolRegistered) return;
_protocolRegistered = true;
maplibregl.addProtocol('sentinel', async (params: { url: string }) => {
// Parse: sentinel://14/8529/5765?preset=TRUE-COLOR&date=2024-06-01
const url = new URL(params.url.replace('sentinel://', 'http://dummy/'));
const parts = url.pathname.split('/').filter(Boolean);
const z = parseInt(parts[0], 10);
const x = parseInt(parts[1], 10);
const y = parseInt(parts[2], 10);
const preset = url.searchParams.get('preset') || 'TRUE-COLOR';
const date = url.searchParams.get('date') || new Date().toISOString().slice(0, 10);
tileLoadStart();
try {
const data = await fetchSentinelTile(z, x, y, preset, date);
if (!data) {
return { data: TRANSPARENT_1X1_PNG };
}
recordTileFetch();
return { data };
} finally {
tileLoadEnd();
}
});
}
// 1×1 transparent PNG (68 bytes)
const TRANSPARENT_1X1_PNG = new Uint8Array([
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44,
0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f,
0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x62, 0x00,
0x00, 0x00, 0x02, 0x00, 0x01, 0xe2, 0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
]).buffer;
/**
* Build a sentinel:// tile URL template for MapLibre.
* MapLibre will substitute {z}, {x}, {y} at render time.
*/
export function buildSentinelTileUrl(preset: string, date: string): string {
return `sentinel://{z}/{x}/{y}?preset=${encodeURIComponent(preset)}&date=${encodeURIComponent(date)}`;
}
// ─── Layer presets ─────────────────────────────────────────────────────────
export const SENTINEL_PRESETS = [
{ id: 'TRUE-COLOR', name: 'True Color (S2)', description: 'Natural color RGB' },
{ id: 'FALSE-COLOR', name: 'False Color IR', description: 'Vegetation analysis' },
{ id: 'NDVI', name: 'NDVI', description: 'Vegetation index' },
{ id: 'MOISTURE-INDEX', name: 'Moisture Index', description: 'Soil/vegetation moisture' },
] as const;
export type SentinelPresetId = (typeof SENTINEL_PRESETS)[number]['id'];
// ─── Usage tracking ───────────────────────────────────────────────────────
const LS_USAGE_KEY = 'sb_sentinel_usage';
interface SentinelUsage {
month: string; // "2026-03"
tiles: number;
pu: number; // tiles * 0.25
}
function currentMonth(): string {
return new Date().toISOString().slice(0, 7);
}
export function getSentinelUsage(): SentinelUsage {
if (typeof window === 'undefined') return { month: currentMonth(), tiles: 0, pu: 0 };
try {
const raw = localStorage.getItem(LS_USAGE_KEY);
if (raw) {
const data = JSON.parse(raw) as SentinelUsage;
// Reset if month changed
if (data.month === currentMonth()) return data;
}
} catch { /* ignore */ }
return { month: currentMonth(), tiles: 0, pu: 0 };
}
export function recordTileFetch(count = 1): void {
const usage = getSentinelUsage();
usage.tiles += count;
usage.pu = Math.round(usage.tiles * 0.25 * 100) / 100;
usage.month = currentMonth();
localStorage.setItem(LS_USAGE_KEY, JSON.stringify(usage));
}
// ─── First-time flag ──────────────────────────────────────────────────────
const LS_SENTINEL_SEEN = 'sb_sentinel_info_seen';
export function hasSentinelInfoBeenSeen(): boolean {
if (typeof window === 'undefined') return true;
return localStorage.getItem(LS_SENTINEL_SEEN) === 'true';
}
export function markSentinelInfoSeen(): void {
localStorage.setItem(LS_SENTINEL_SEEN, 'true');
}
// ─── Tile loading tracker ────────────────────────────────────────────────
type LoadingListener = (inflight: number, loaded: number) => void;
let _inflight = 0;
let _loaded = 0;
let _listeners: LoadingListener[] = [];
/** Subscribe to tile loading state changes. Returns unsubscribe function. */
export function onTileLoadingChange(cb: LoadingListener): () => void {
_listeners.push(cb);
return () => { _listeners = _listeners.filter(l => l !== cb); };
}
function _notifyListeners() {
for (const cb of _listeners) cb(_inflight, _loaded);
}
export function tileLoadStart(): void {
_inflight++;
_notifyListeners();
}
export function tileLoadEnd(): void {
_inflight = Math.max(0, _inflight - 1);
_loaded++;
_notifyListeners();
}
export function resetTileLoading(): void {
_inflight = 0;
_loaded = 0;
_notifyListeners();
}
export function getTileLoadingState(): { inflight: number; loaded: number } {
return { inflight: _inflight, loaded: _loaded };
}
@@ -0,0 +1,43 @@
import { randomUUID } from 'crypto';
type AdminSessionRecord = {
adminKey: string;
expiresAt: number;
};
const sessions = new Map<string, AdminSessionRecord>();
function purgeExpiredSessions() {
const now = Date.now();
for (const [token, session] of sessions.entries()) {
if (session.expiresAt <= now) {
sessions.delete(token);
}
}
}
export function createAdminSessionToken(adminKey: string, maxAgeSeconds: number): string {
purgeExpiredSessions();
const token = randomUUID();
sessions.set(token, {
adminKey,
expiresAt: Date.now() + maxAgeSeconds * 1000,
});
return token;
}
export function resolveAdminSessionToken(token: string): string {
purgeExpiredSessions();
const session = sessions.get(token);
if (!session) return '';
return session.adminKey;
}
export function hasAdminSessionToken(token: string): boolean {
return Boolean(resolveAdminSessionToken(token));
}
export function clearAdminSessionToken(token: string): void {
if (!token) return;
sessions.delete(token);
}
+43
View File
@@ -0,0 +1,43 @@
import { API_BASE } from '@/lib/api';
import type {
ShodanCountResponse,
ShodanHostResponse,
ShodanSearchResponse,
ShodanStatusResponse,
} from '@/types/shodan';
type JsonBody = Record<string, unknown>;
async function postJson<T>(path: string, body: JsonBody): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(String((data as { detail?: string }).detail || 'Request failed'));
}
return data as T;
}
export async function fetchShodanStatus(): Promise<ShodanStatusResponse> {
const res = await fetch(`${API_BASE}/api/tools/shodan/status`);
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(String((data as { detail?: string }).detail || 'Failed to load Shodan status'));
}
return data as ShodanStatusResponse;
}
export function searchShodan(query: string, page = 1, facets: string[] = []) {
return postJson<ShodanSearchResponse>('/api/tools/shodan/search', { query, page, facets });
}
export function countShodan(query: string, facets: string[] = []) {
return postJson<ShodanCountResponse>('/api/tools/shodan/count', { query, facets });
}
export function lookupShodanHost(ip: string, history = false) {
return postJson<ShodanHostResponse>('/api/tools/shodan/host', { ip, history });
}
File diff suppressed because one or more lines are too long
+36
View File
@@ -0,0 +1,36 @@
import { API_BASE } from '@/lib/api';
import type {
UWCongressResponse,
UWInsiderResponse,
UWStatusResponse,
} from '@/types/unusualWhales';
async function postJson<T>(path: string): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}',
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(String((data as { detail?: string }).detail || 'Request failed'));
}
return data as T;
}
export async function fetchUWStatus(): Promise<UWStatusResponse> {
const res = await fetch(`${API_BASE}/api/tools/uw/status`);
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(String((data as { detail?: string }).detail || 'Failed to load Finnhub status'));
}
return data as UWStatusResponse;
}
export function fetchCongressTrades() {
return postJson<UWCongressResponse>('/api/tools/uw/congress');
}
export function fetchInsiderTransactions() {
return postJson<UWInsiderResponse>('/api/tools/uw/darkpool');
}
+129
View File
@@ -0,0 +1,129 @@
export type ViewBounds = {
south: number;
west: number;
north: number;
east: number;
};
const EARTH_RADIUS_MILES = 3958.7613;
const RAD_TO_DEG = 180 / Math.PI;
const DEG_TO_RAD = Math.PI / 180;
export const DEFAULT_PRELOAD_RADIUS_MILES = 3000;
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
export function normalizeLongitude(value: number): number {
if (!Number.isFinite(value)) return 0;
let normalized = ((value + 180) % 360 + 360) % 360 - 180;
if (normalized === -180 && value > 0) normalized = 180;
return normalized;
}
export function normalizeViewBounds(bounds: ViewBounds): ViewBounds {
const south = clamp(bounds.south, -90, 90);
const north = clamp(bounds.north, -90, 90);
const rawWidth = Math.abs(bounds.east - bounds.west);
if (!Number.isFinite(rawWidth) || rawWidth >= 360) {
return { south, west: -180, north, east: 180 };
}
const west = normalizeLongitude(bounds.west);
const east = normalizeLongitude(bounds.east);
if (east < west) {
return { south, west: -180, north, east: 180 };
}
return { south, west, north, east };
}
export function expandBoundsToRadius(
bounds: ViewBounds,
radiusMiles: number = DEFAULT_PRELOAD_RADIUS_MILES,
): ViewBounds {
const normalized = normalizeViewBounds(bounds);
if (!Number.isFinite(radiusMiles) || radiusMiles <= 0) return normalized;
if (normalized.west === -180 && normalized.east === 180) return normalized;
const centerLat = (normalized.south + normalized.north) / 2;
const centerLng = (normalized.west + normalized.east) / 2;
const angularDistance = radiusMiles / EARTH_RADIUS_MILES;
const latDelta = angularDistance * RAD_TO_DEG;
const centerLatRad = centerLat * DEG_TO_RAD;
const cosLat = Math.cos(centerLatRad);
let lngDelta = 180;
if (cosLat > 1e-6) {
const ratio = Math.sin(angularDistance) / cosLat;
lngDelta = Math.min(180, Math.asin(Math.min(1, Math.max(-1, ratio))) * RAD_TO_DEG);
}
const south = clamp(Math.min(normalized.south, centerLat - latDelta), -90, 90);
const north = clamp(Math.max(normalized.north, centerLat + latDelta), -90, 90);
const radiusWest = centerLng - lngDelta;
const radiusEast = centerLng + lngDelta;
if (radiusWest < -180 || radiusEast > 180) {
return { south, west: -180, north, east: 180 };
}
return normalizeViewBounds({
south,
west: Math.min(normalized.west, radiusWest),
north,
east: Math.max(normalized.east, radiusEast),
});
}
function quantizationStep(bounds: ViewBounds): number {
const latSpan = Math.abs(bounds.north - bounds.south);
const lngSpan = Math.abs(bounds.east - bounds.west);
const span = Math.max(latSpan, lngSpan);
if (!Number.isFinite(span) || span >= 180) return 5;
if (span >= 80) return 2;
if (span >= 20) return 1;
if (span >= 8) return 0.25;
if (span >= 3) return 0.1;
return 0.05;
}
function decimalsForStep(step: number): number {
if (step >= 1) return 0;
if (step >= 0.25) return 2;
return 2;
}
function floorToStep(value: number, step: number): number {
return Math.floor(value / step) * step;
}
function ceilToStep(value: number, step: number): number {
return Math.ceil(value / step) * step;
}
function outwardRound(bounds: ViewBounds, step: number): ViewBounds {
const digits = decimalsForStep(step);
const south = Number(clamp(floorToStep(bounds.south, step), -90, 90).toFixed(digits));
const west = Number(clamp(floorToStep(bounds.west, step), -180, 180).toFixed(digits));
const north = Number(clamp(ceilToStep(bounds.north, step), -90, 90).toFixed(digits));
const east = Number(clamp(ceilToStep(bounds.east, step), -180, 180).toFixed(digits));
if (east - west >= 360) {
return { south, west: -180, north, east: 180 };
}
return { south, west, north, east };
}
export function coarsenViewBounds(bounds: ViewBounds): ViewBounds {
const normalized = normalizeViewBounds(bounds);
if (normalized.west === -180 && normalized.east === 180) {
return normalized;
}
return outwardRound(normalized, quantizationStep(normalized));
}
export function buildBoundsQuery(bounds: ViewBounds | null | undefined): string {
if (!bounds) return '';
const coarse = coarsenViewBounds(bounds);
const step = quantizationStep(coarse);
const digits = decimalsForStep(step);
return `?s=${coarse.south.toFixed(digits)}&w=${coarse.west.toFixed(digits)}&n=${coarse.north.toFixed(digits)}&e=${coarse.east.toFixed(digits)}`;
}