Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ PUBLIC_SITE_URL=https://openshock.app
PUBLIC_SITE_SHORT_URL=https://openshock.app
PUBLIC_BACKEND_API_URL=https://api.openshock.app
PUBLIC_GATEWAY_CSP_WILDCARD=https://*.openshock.app
PUBLIC_FIRMWARE_REPO_URL=https://repo.openshock.org

# Server-side only (Node adapter). When set to `true`, disables TLS certificate
# validation for the server's own outgoing requests, allowing SSR/API calls to a
Expand All @@ -41,4 +42,4 @@ PUBLIC_SIGNOZ_TRACE_PROPAGATION=false
PUBLIC_SIGNOZ_DEPLOYMENT_ENVIRONMENT=
# Extra OTel resource attributes, comma-separated key=value pairs (same format as the standard
# OTEL_RESOURCE_ATTRIBUTES env var). Example: deployment.region=eu,team=frontend
PUBLIC_SIGNOZ_RESOURCE_ATTRIBUTES=
PUBLIC_SIGNOZ_RESOURCE_ATTRIBUTES=
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ PUBLIC_SITE_URL=https://openshock.dev
PUBLIC_SITE_SHORT_URL=https://openshock.dev
PUBLIC_BACKEND_API_URL=https://api.openshock.dev
PUBLIC_GATEWAY_CSP_WILDCARD=https://*.openshock.dev
PUBLIC_FIRMWARE_REPO_URL=https://repo.openshock.dev

PUBLIC_TURNSTILE_DEV_BYPASS_VALUE=dev-bypass
PUBLIC_DEVELOPMENT_BANNER=true
125 changes: 0 additions & 125 deletions src/lib/api/firmwareCDN.ts

This file was deleted.

111 changes: 111 additions & 0 deletions src/lib/api/firmwareRepo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { PUBLIC_FIRMWARE_REPO_URL } from '$env/static/public';
import { HashBuffer } from '@openshock/svelte-core/utils/crypto.js';

export const FirmwareChannels = ['stable', 'beta', 'develop'] as const;
export type FirmwareChannel = (typeof FirmwareChannels)[number];

export interface FirmwareArtifact {
type: string;
url: string;
sha256Hash: string;
fileSize: number;
}

export interface FirmwareBoard {
chip: string;
deprecated: boolean;
artifacts: FirmwareArtifact[];
}

export interface FirmwareRelease {
version: string;
channel: string;
releaseDate: string;
changelog: string;
boards: Record<string, FirmwareBoard>;
}

export interface FirmwareUpdateResponse {
version: string;
artifact: FirmwareArtifact;
}

export interface FirmwareVersionSummary {
version: string;
channel: string;
releaseDate: string;
changelog: string;
}

const BASE_URL = PUBLIC_FIRMWARE_REPO_URL.replace(/\/+$/, '');

export async function FetchLatest(channel: FirmwareChannel): Promise<FirmwareRelease> {
const response = await fetch(`${BASE_URL}/v2/firmware/latest/${channel}`);
if (!response.ok)
throw new Error(`Failed to fetch latest firmware: ${response.status} ${response.statusText}`);
return await response.json();
}

export async function FetchVersion(
channel: FirmwareChannel,
version: string
): Promise<FirmwareRelease> {
const response = await fetch(`${BASE_URL}/v2/firmware/versions/${channel}/${version}`);
if (!response.ok)
throw new Error(`Failed to fetch firmware version: ${response.status} ${response.statusText}`);
return await response.json();
}

export async function FetchVersionHistory(
channel: FirmwareChannel,
limit = 20,
offset = 0
): Promise<{ versions: FirmwareVersionSummary[]; total: number }> {
const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
const response = await fetch(`${BASE_URL}/v2/firmware/versions/${channel}?${params}`);
if (!response.ok)
throw new Error(`Failed to fetch version history: ${response.status} ${response.statusText}`);
return await response.json();
}

export function ExtractBoards(
release: FirmwareRelease,
chip?: string | null,
includeDeprecated = false
): string[] {
const entries = Object.entries(release.boards);
const filtered = entries.filter(([, board]) => {
if (!includeDeprecated && board.deprecated) return false;
if (chip && board.chip !== chip) return false;
return true;
});
return filtered.map(([name]) => name).sort();
}

export function FindArtifact(
release: FirmwareRelease,
board: string,
type: string
): FirmwareArtifact | null {
const boardInfo = release.boards[board];
if (!boardInfo) return null;
return boardInfo.artifacts.find((a) => a.type === type) ?? null;
}

async function DownloadBinary(url: string): Promise<Uint8Array> {
const response = await fetch(url);
if (!response.ok)
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
return await response.bytes();
}
Comment on lines +95 to +100

export async function DownloadAndVerifyArtifact(artifact: FirmwareArtifact): Promise<Uint8Array> {
const binary = await DownloadBinary(artifact.url);

const calculatedHash = await HashBuffer(binary.buffer as ArrayBuffer, 'SHA-256');
if (calculatedHash.toUpperCase() !== artifact.sha256Hash.toUpperCase()) {
throw new Error(`Hash mismatch: expected ${artifact.sha256Hash}, got ${calculatedHash}`);
}

return binary;
}
38 changes: 24 additions & 14 deletions src/lib/components/FirmwareChannelSelector.svelte
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
<script lang="ts">
import { CircleCheckBig, TriangleAlert } from '@lucide/svelte';
import {
FetchChannelVersion,
FetchLatest,
type FirmwareChannel,
FirmwareChannels,
} from '$lib/api/firmwareCDN';
type FirmwareRelease,
} from '$lib/api/firmwareRepo';
import * as ToggleGroup from '@openshock/svelte-core/components/ui/toggle-group';
import { handleApiError } from '$lib/errorhandling/apiErrorHandling';

/** Optional chip to constrain the list of boards to */
//export let chip: string | null = null;
interface Props {
channel?: FirmwareChannel;
version?: string | null;
latestResponse?: FirmwareRelease | null;
disabled?: boolean;
}

let {
channel = $bindable<FirmwareChannel>('stable'),
// eslint-disable-next-line no-useless-assignment -- $bindable fallback, not a dead assignment
version = $bindable(null),
// eslint-disable-next-line no-useless-assignment -- $bindable fallback, not a dead assignment
latestResponse = $bindable(null),
disabled = false,
}: Props = $props();

let versions = $state<{ [key in FirmwareChannel]?: string | null }>({});
let cache = $state<{ [key in FirmwareChannel]?: FirmwareRelease | null }>({});

// Reactive effect to update the version based on the selected channel.
$effect(() => {
Expand All @@ -32,22 +35,29 @@
}

// Use a cached value if available.
const cachedVersion = versions[channel];
if (cachedVersion !== undefined) {
version = cachedVersion;
const cached = cache[channel];
if (cached !== undefined) {
latestResponse = cached;
version = cached?.version ?? null;
return;
}

// Capture the current channel to avoid race conditions if channel changes.
const currentChannel = channel;

Comment on lines 45 to 47
// Fetch the channel version from the API.
FetchChannelVersion(currentChannel)
.then((ver) => {
version = ver ?? null;
versions[currentChannel] = version;
// Fetch the latest firmware info from the repository server.
FetchLatest(currentChannel)
.then((resp) => {
latestResponse = resp;
version = resp.version;
cache[currentChannel] = resp;
})
.catch(handleApiError);
.catch((error) => {
latestResponse = null;
version = null;
cache[currentChannel] = null;
handleApiError(error);
});
});
</script>

Expand Down
2 changes: 1 addition & 1 deletion src/routes/(app)/hubs/[hubId=guid]/update/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
import { cn } from '@openshock/svelte-core/utils/shadcn.js';
import { NumberToHexPadded } from '@openshock/svelte-core/utils/convert.js';
import { onMount } from 'svelte';
import type { FirmwareChannel } from '$lib/api/firmwareCDN';
import type { FirmwareChannel } from '$lib/api/firmwareRepo';
import { PageHeader } from '@openshock/svelte-core/components';

let hubLoaded = $state(false);
Expand Down
10 changes: 6 additions & 4 deletions src/routes/terminal/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
Zap,
} from '@lucide/svelte';
import { browser } from '$app/env';
import type { FirmwareChannel } from '$lib/api/firmwareCDN';
import type { FirmwareChannel, FirmwareRelease } from '$lib/api/firmwareRepo';
import { Container } from '@openshock/svelte-core/components';
import FirmwareChannelSelector from '$lib/components/FirmwareChannelSelector.svelte';
import { ChromeLogo } from '@openshock/svelte-core/components/svg';
Expand Down Expand Up @@ -46,6 +46,7 @@

let channel = $state<FirmwareChannel>('stable');
let version = $state<string | null>(null);
let latestResponse = $state<FirmwareRelease | null>(null);
// Tracks which channel+version the user explicitly confirmed. Changing either invalidates it.
let confirmedChannel = $state<FirmwareChannel | null>(null);
let confirmedVersion = $state<string | null>(null);
Expand Down Expand Up @@ -325,6 +326,7 @@
<FirmwareChannelSelector
bind:channel
bind:version
bind:latestResponse
disabled={isFlashing}
/>
{#if version}
Expand Down Expand Up @@ -352,7 +354,7 @@
{#if isCurrent}
<div class="flex flex-col gap-4">
<FirmwareBoardSelector
{version}
{latestResponse}
bind:selectedBoard={board}
disabled={isFlashing}
/>
Expand Down Expand Up @@ -381,9 +383,9 @@

<!-- Step 3: Flash -->
{#if i === 2}
{#if isCurrent && version && board && connection}
{#if isCurrent && latestResponse && board && connection}
<FirmwareFlasher
{version}
{latestResponse}
{board}
{connection}
{eraseBeforeFlash}
Expand Down
Loading