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.

9 changes: 9 additions & 0 deletions src/lib/api/firmwareRepo/ResponseError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export class ResponseError extends Error {
override name = 'ResponseError';
constructor(
public response: Response,
msg?: string
) {
super(msg ?? `HTTP ${response.status} ${response.statusText}`);
}
}
6 changes: 6 additions & 0 deletions src/lib/api/firmwareRepo/TransformError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class TransformError extends Error {
override name = 'TransformError';
constructor(message: string) {
super(message);
}
}
37 changes: 37 additions & 0 deletions src/lib/api/firmwareRepo/artifacts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { HashBuffer } from '@openshock/svelte-core/utils';
import type { FirmwareArtifact, FirmwareRelease } from './models';

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}`);
}

if ('bytes' in Response.prototype) {
return await response.bytes();
}

const buf = await response.arrayBuffer();
return new Uint8Array(buf);
}

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: 38 additions & 0 deletions src/lib/api/firmwareRepo/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getFirmwareRepoURL, type FirmwareRepoPath } from '$lib/utils/url';
import { ResponseError } from './ResponseError';

/**
* Fetches JSON from the firmware repository server.
*
* Unlike the backend client this sends no credentials — the repository server is a public,
* cross-origin, read-only service.
*
* Pass a `URL` instead of a path when the request needs query parameters; build it with
* {@link getFirmwareRepoURL} and set them via `searchParams`.
*/
export async function GetJson<T>(
path: FirmwareRepoPath | URL,
expectedStatus = 200,
transformer: (data: unknown) => T
): Promise<T> {
const url = path instanceof URL ? path : getFirmwareRepoURL(path);

const res = await fetch(url, {
method: 'GET',
headers: { accept: 'application/json' },
redirect: 'error',
});

if (res.status !== expectedStatus) {
throw new ResponseError(res, `Unexpected status ${res.status} for GET ${url.href}`);
}

const contentType = res.headers.get('content-type') ?? '';
if (!contentType.includes('application/json')) {
throw new ResponseError(res, `Expected JSON but got ${contentType}`);
}

const data = await res.json();

return transformer(data);
}
21 changes: 21 additions & 0 deletions src/lib/api/firmwareRepo/boards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { FirmwareRelease } from './models';

/**
* Board names available in a release, optionally narrowed to a chip.
*
* `chip` is matched against the chip's esptool-js name, which is what a connected-device
* detection returns.
*/
export function ExtractBoards(
release: FirmwareRelease,
chip?: string | null,
includeDiscontinued = false
): string[] {
const entries = Object.entries(release.boards);
const filtered = entries.filter(([, board]) => {
if (!includeDiscontinued && board.discontinued) return false;
if (chip && board.chip.name !== chip) return false;
return true;
});
return filtered.map(([name]) => name).sort();
}
57 changes: 57 additions & 0 deletions src/lib/api/firmwareRepo/firmware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { getFirmwareRepoURL } from '$lib/utils/url';
import { GetJson } from './base';
import type {
FirmwareBoardRelease,
FirmwareChannel,
FirmwareRelease,
FirmwareVersionHistory,
} from './models';
import {
TransformFirmwareBoardRelease,
TransformFirmwareRelease,
TransformFirmwareVersionHistory,
} from './transformers';

/** Most recent published release for a channel, with every board. */
export async function FetchLatest(channel: FirmwareChannel): Promise<FirmwareRelease> {
return GetJson(`2/firmware/latest/${channel}`, 200, TransformFirmwareRelease);
}

/**
* Full release details for one version. Versions are globally unique, so this is not
* scoped by channel.
*/
export async function FetchVersion(version: string): Promise<FirmwareRelease> {
return GetJson(
`2/firmware/versions/${encodeURIComponent(version)}`,
200,
TransformFirmwareRelease
);
}

/** Artifacts for a single board in a single version. `board` is the board name. */
export async function FetchBoardRelease(
version: string,
board: string
): Promise<FirmwareBoardRelease> {
return GetJson(
`2/firmware/versions/${encodeURIComponent(version)}/${encodeURIComponent(board)}`,
200,
TransformFirmwareBoardRelease
);
}

/** Paginated version history. Channel is an optional filter, passed as a query parameter. */
export async function FetchVersionHistory(
channel?: FirmwareChannel | null,
limit = 20,
offset = 0
): Promise<FirmwareVersionHistory> {
const url = getFirmwareRepoURL('2/firmware/versions');

url.searchParams.set('limit', String(limit));
url.searchParams.set('offset', String(offset));
if (channel) url.searchParams.set('channel', channel);

return GetJson(url, 200, TransformFirmwareVersionHistory);
}
6 changes: 6 additions & 0 deletions src/lib/api/firmwareRepo/models/FirmwareArtifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface FirmwareArtifact {
type: string;
url: string;
sha256Hash: string;
fileSize: number;
}
8 changes: 8 additions & 0 deletions src/lib/api/firmwareRepo/models/FirmwareBoard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { FirmwareArtifact } from './FirmwareArtifact';
import type { FirmwareChipRef } from './FirmwareChipRef';

export interface FirmwareBoard {
chip: FirmwareChipRef;
discontinued: boolean;
artifacts: FirmwareArtifact[];
}
8 changes: 8 additions & 0 deletions src/lib/api/firmwareRepo/models/FirmwareBoardRelease.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type { FirmwareArtifact } from './FirmwareArtifact';

/** Minimal single-board response. `boardId` is the canonical board name. */
export interface FirmwareBoardRelease {
version: string;
boardId: string;
artifacts: FirmwareArtifact[];
}
3 changes: 3 additions & 0 deletions src/lib/api/firmwareRepo/models/FirmwareChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const FirmwareChannels = ['stable', 'beta', 'develop'] as const;

export type FirmwareChannel = (typeof FirmwareChannels)[number];
7 changes: 7 additions & 0 deletions src/lib/api/firmwareRepo/models/FirmwareChipRef.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Chip reference. `name` is the public identifier and matches esptool-js chip identifiers exactly
* (e.g. "ESP32-S3") — pass it straight to esptool-js. The chip's UUID is server-internal.
*/
export interface FirmwareChipRef {
name: string;
}
13 changes: 13 additions & 0 deletions src/lib/api/firmwareRepo/models/FirmwareRelease.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { FirmwareBoard } from './FirmwareBoard';
import type { FirmwareReleaseNote } from './FirmwareReleaseNote';
import type { FirmwareSource } from './FirmwareSource';

export interface FirmwareRelease {
version: string;
channel: string;
releaseDate: Temporal.Instant;
source: FirmwareSource;
releaseNotes: FirmwareReleaseNote[];
/** Keyed by canonical board name, e.g. "Wemos-D1-Mini-ESP32". */
boards: Record<string, FirmwareBoard>;
}
7 changes: 7 additions & 0 deletions src/lib/api/firmwareRepo/models/FirmwareReleaseNote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { FirmwareReleaseNoteType } from './FirmwareReleaseNoteType';

export interface FirmwareReleaseNote {
type: FirmwareReleaseNoteType;
title: string | null;
content: string;
}
3 changes: 3 additions & 0 deletions src/lib/api/firmwareRepo/models/FirmwareReleaseNoteType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const FirmwareReleaseNoteTypes = ['breaking', 'warning', 'info', 'section'] as const;

export type FirmwareReleaseNoteType = (typeof FirmwareReleaseNoteTypes)[number];
6 changes: 6 additions & 0 deletions src/lib/api/firmwareRepo/models/FirmwareRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface FirmwareRepository {
id: string;
provider: string;
owner: string;
repo: string;
}
Loading