32 Commits

Author SHA1 Message Date
Renovate Bot
63cfe3f87c Update dependency tailwindcss to v4
Some checks failed
renovate/artifacts Artifact file update failure
CI / build (pull_request) Failing after 4m43s
2025-12-30 00:03:29 +00:00
931c8f4370 Merge pull request 'Update https://git.quad4.io/actions/setup-pnpm action to v4.2.0' (#16) from renovate/https-git.quad4.io-actions-setup-pnpm-4.x into master
All checks were successful
CI / build (push) Successful in 1m26s
renovate / renovate (push) Successful in 5m32s
Reviewed-on: #16
2025-12-29 20:07:19 +00:00
2c5e258934 Merge pull request 'Update https://git.quad4.io/actions/setup-node action to v4.4.0' (#15) from renovate/https-git.quad4.io-actions-setup-node-4.x into master
All checks were successful
CI / build (push) Successful in 1m36s
renovate / renovate (push) Successful in 6m26s
Reviewed-on: #15
2025-12-29 20:07:11 +00:00
Renovate Bot
4ba4ecd722 Update https://git.quad4.io/actions/setup-pnpm action to v4.2.0
All checks were successful
CI / build (pull_request) Successful in 9m47s
2025-12-29 20:04:50 +00:00
Renovate Bot
812b47457d Update https://git.quad4.io/actions/setup-node action to v4.4.0
All checks were successful
CI / build (pull_request) Successful in 9m44s
2025-12-29 20:04:49 +00:00
5ec4eddd14 Merge pull request 'Update https://git.quad4.io/actions/checkout action to v4.3.1' (#11) from renovate/https-git.quad4.io-actions-checkout-4.x into master
Some checks failed
CI / build (push) Failing after 1m32s
renovate / renovate (push) Failing after 3m51s
Reviewed-on: #11
2025-12-29 20:01:39 +00:00
37012ac80a Merge pull request 'Update https://git.quad4.io/actions/setup-pnpm digest to a7487c7' (#9) from renovate/https-git.quad4.io-actions-setup-pnpm-digest into master
All checks were successful
renovate / renovate (push) Successful in 6m3s
CI / build (push) Successful in 9m44s
Reviewed-on: #9
2025-12-29 20:01:30 +00:00
69e551986a Merge pull request 'Update https://git.quad4.io/actions/setup-go action to v5.6.0' (#12) from renovate/https-git.quad4.io-actions-setup-go-5.x into master
Some checks failed
CI / build (push) Successful in 1m37s
renovate / renovate (push) Failing after 4m14s
Reviewed-on: #12
2025-12-29 20:01:20 +00:00
ivan
f8254c735b Merge pull request 'Update ghcr.io/renovatebot/renovate Docker tag to v42.66.11' (#10) from renovate/ghcr.io-renovatebot-renovate-42.x into master
All checks were successful
renovate / renovate (push) Successful in 4m57s
CI / build (push) Successful in 9m45s
Reviewed-on: #10
2025-12-29 00:21:15 +00:00
Renovate Bot
046e55f6d0 Update https://git.quad4.io/actions/setup-go action to v5.6.0
All checks were successful
CI / build (pull_request) Successful in 1m44s
2025-12-29 00:04:00 +00:00
Renovate Bot
21bef6d2d7 Update https://git.quad4.io/actions/checkout action to v4.3.1
Some checks failed
CI / build (pull_request) Failing after 1m45s
2025-12-29 00:03:57 +00:00
Renovate Bot
c5c6bea16e Update ghcr.io/renovatebot/renovate Docker tag to v42.66.11
All checks were successful
CI / build (pull_request) Successful in 9m44s
2025-12-29 00:03:56 +00:00
Renovate Bot
0865a2bcbc Update https://git.quad4.io/actions/setup-pnpm digest to a7487c7
All checks were successful
CI / build (pull_request) Successful in 9m48s
2025-12-28 14:51:41 +00:00
cf676eb14c Update Gitea publish workflow by adding input for tag name in release process
All checks were successful
renovate / renovate (push) Successful in 5m48s
CI / build (push) Successful in 9m44s
2025-12-28 08:47:40 -06:00
ivan
f8733d8e6f Merge pull request 'Update ghcr.io/renovatebot/renovate Docker tag to v42' (#4) from renovate/ghcr.io-renovatebot-renovate-42.x into master
Some checks failed
renovate / renovate (push) Has been cancelled
CI / build (push) Successful in 9m45s
Reviewed-on: #4
2025-12-28 06:59:44 +00:00
8204dbf811 Update Taskfile.yml
Some checks failed
renovate / renovate (push) Has been cancelled
CI / build (push) Has been cancelled
2025-12-28 00:56:49 -06:00
0fb281a783 Update Gitea workflow to specify exact version for task setup action and adjust versioning format
All checks were successful
CI / build (push) Successful in 1m0s
renovate / renovate (push) Successful in 5m11s
2025-12-28 00:50:08 -06:00
31948b8f9e Update Gitea workflow to use a custom action URL for checkout
Some checks failed
CI / build (push) Successful in 1m7s
renovate / renovate (push) Failing after 4m7s
2025-12-27 23:28:20 -06:00
6c23005368 Update Renovate configuration for Gitea workflows by adding regex managers for custom action URLs, including support for checkout, Go, Node, pnpm, task setup, SBOM generation, and release actions.
Some checks failed
renovate / renovate (push) Failing after 1s
CI / build (push) Successful in 1m9s
2025-12-27 23:27:40 -06:00
8998cc8253 Update Gitea CI workflow to use custom action URLs for checkout, Go, Node, and pnpm setup
Some checks failed
renovate / renovate (push) Failing after 1s
CI / build (push) Successful in 1m51s
2025-12-27 23:24:54 -06:00
0f250bc715 Remove
Some checks failed
CI / build (push) Failing after 6s
renovate / renovate (push) Failing after 6s
2025-12-27 23:19:06 -06:00
b9119877d0 Update Gitea workflows to use actions for pnpm, task setup, SBOM generation, and release creation 2025-12-27 23:18:59 -06:00
4ecf6a921c Update version number to 0.4.1 in package.json
Some checks failed
Publish / publish (push) Failing after 41s
renovate / renovate (push) Successful in 1m1s
CI / build (push) Successful in 1m11s
2025-12-27 23:02:08 -06:00
1d8fadd835 Add Gitea workflow for publishing with Go and Node setup
All checks were successful
renovate / renovate (push) Successful in 49s
CI / build (push) Successful in 1m14s
2025-12-27 23:01:26 -06:00
55eaf28514 Update error handling in verifier and improve error message display in VerificationModal
All checks were successful
CI / build (push) Successful in 1m0s
renovate / renovate (push) Successful in 1m14s
- Updated the error handling in loadVerifier to log detailed errors and provide clearer feedback on WASM script loading issues.
- Modified the error message display in VerificationModal to better format and separate error details for improved user experience.
2025-12-27 22:45:57 -06:00
1687815aad Update WebAssembly verifier and JavaScript execution environment
All checks were successful
CI / build (push) Successful in 1m0s
renovate / renovate (push) Successful in 1m5s
- Updated the integrity hash for the WebAssembly module in verifier.ts to ensure security compliance.
- Made various formatting improvements in wasm_exec.js for consistency, including string usage and whitespace cleanup.
- Removed redundant comments and streamlined function definitions for better readability.
2025-12-27 22:43:32 -06:00
ff35c0ec01 Add static/verifier to .prettierignore for improved formatting control 2025-12-27 22:42:12 -06:00
7cd4c58927 Update global and API rate limits for improved performance
All checks were successful
renovate / renovate (push) Successful in 58s
CI / build (push) Successful in 59s
- Increased GlobalRateLimit from 500 to 2000 to better handle higher traffic.
- Raised APIRateLimit from 150 to 500 to enhance API responsiveness.
2025-12-27 22:38:37 -06:00
9fb84eb228 Update global and API rate limits for enhanced performance
- Increased GlobalRateLimit from 100 to 500 to accommodate higher traffic.
- Raised APIRateLimit from 30 to 150 to improve API responsiveness and user experience.
2025-12-27 22:38:27 -06:00
0dfbacce37 Update README and frontend files for improved clarity and functionality
All checks were successful
CI / build (push) Successful in 1m0s
renovate / renovate (push) Successful in 1m6s
- Added a note in the README about using Taskfile for project management.
- Removed the crossOrigin attribute from the WebAssembly fetch request in verifier.ts for security compliance.
- Refactored the wasm_exec.js file for consistency in string usage and improved readability.
- Cleaned up whitespace in the SRI generation script to enhance code clarity.
2025-12-27 22:37:27 -06:00
e2c80671fa Add "Task" build and development process with Taskfile integration
Some checks failed
CI / build (push) Failing after 30s
renovate / renovate (push) Successful in 1m25s
- Added Taskfile.yml to streamline build, development, and testing tasks.
- Updated README to reflect new build instructions and development environment setup using `go-task`.
- Included `.taskfile.env` and `.task` in .dockerignore and .gitignore for better environment management.
- Modified asset loading in verifier.ts to include integrity and cross-origin attributes for security.
- Updated SRI generation script to handle both directory and single file inputs for improved flexibility.
2025-12-27 22:35:12 -06:00
Renovate Bot
fe0dd110f4 Update ghcr.io/renovatebot/renovate Docker tag to v42
All checks were successful
CI / build (pull_request) Successful in 10m58s
2025-12-27 22:30:17 +00:00
18 changed files with 494 additions and 318 deletions

View File

@@ -26,3 +26,5 @@ test-hashes.json
test_updater.txt
test_handlers_hashes.json
.taskfile.env
.task

View File

@@ -16,21 +16,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Setup Go
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: '1.25.4'
cache: true
- name: Setup Node
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
uses: https://git.quad4.io/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '22'
- name: Setup pnpm
uses: pnpm/action-setup@7088e561eb65bb68695d245aa206f005ef30921d # v4.1.0
uses: https://git.quad4.io/actions/setup-pnpm@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 9

View File

@@ -0,0 +1,61 @@
name: Publish
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag_name:
description: 'Tag name for the release'
required: true
type: string
jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Setup Go
uses: https://git.quad4.io/actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: '1.25.4'
cache: true
- name: Setup Node
uses: https://git.quad4.io/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '22'
- name: Setup pnpm
uses: https://git.quad4.io/actions/setup-pnpm@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
with:
version: 10
- name: Install Task
uses: https://git.quad4.io/actions/setup-task@0ab1b2a65bc55236a3bc64cde78f80e20e8885c2 # v1
with:
version: '3.46.3'
- name: Generate SBOM
uses: https://git.quad4.io/actions/gh-gomod-generate-sbom@efc74245d6802c8cefd925620515442756c70d8f # v2
with:
version: v1
args: mod -licenses -json -output bom.json
- name: Build Everything
run: task all
env:
NODE_ENV: production
- name: Create Release and Upload Assets
uses: https://git.quad4.io/actions/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
tag_name: ${{ github.event.inputs.tag_name || github.ref_name }}
files: |
software-station
bom.json
env:
GITHUB_TOKEN: ${{ secrets.GITEA_TOKEN }}

View File

@@ -12,9 +12,9 @@ on:
jobs:
renovate:
runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:37.440.7
container: ghcr.io/renovatebot/renovate:42.66.11
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- name: Fetch remote configuration
run: curl -sL https://git.quad4.io/Quad4-Extra/renovate-config/raw/branch/master/config.js -o config.js
- run: renovate

2
.gitignore vendored
View File

@@ -31,3 +31,5 @@ test_handlers_hashes.json
.DS_Store
Thumbs.db
.taskfile.env
.task

View File

@@ -1,74 +0,0 @@
.PHONY: all build-frontend build-go build-wasm clean release run lint scan check format tidy test test-wasm dev docker-build
BINARY_NAME=software-station
FRONTEND_DIR=frontend
BUILD_DIR=build
VERIFIER_DIR=software-verifier
WASM_OUT=frontend/static/verifier
VERSION=$(shell grep '"version":' $(FRONTEND_DIR)/package.json | cut -d'"' -f4)
BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
VCS_REF=$(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
all: build-wasm build-frontend build-go
dev:
@echo "Starting development environment..."
@pnpm --prefix $(FRONTEND_DIR) dev & go run main.go
build-wasm:
@echo "Building WASM verifier..."
mkdir -p $(WASM_OUT)
cp "$(shell go env GOROOT)/lib/wasm/wasm_exec.js" $(WASM_OUT)/wasm_exec.js
cd $(VERIFIER_DIR) && GOOS=js GOARCH=wasm go build -o ../$(WASM_OUT)/verifier.wasm .
build-frontend: build-wasm
cd $(FRONTEND_DIR) && pnpm install && pnpm build
@echo "Injecting SRI hashes..."
go run scripts/sri-gen/main.go
build-go:
go build -o $(BINARY_NAME) main.go
release: build-frontend
CGO_ENABLED=0 go build -ldflags="-s -w" -o $(BINARY_NAME) main.go
@echo "Release build complete: $(BINARY_NAME)"
run: all
./$(BINARY_NAME)
format:
go fmt ./...
cd $(FRONTEND_DIR) && pnpm run format
lint:
go vet ./...
cd $(FRONTEND_DIR) && pnpm run lint
scan:
gosec ./...
check:
cd $(FRONTEND_DIR) && pnpm run check
tidy: format lint check
test: test-wasm
go test -v -coverpkg=./... ./...
test-wasm:
cd $(VERIFIER_DIR) && go test -v ./...
clean:
rm -rf $(FRONTEND_DIR)/build
rm -rf $(WASM_OUT)
rm -f $(BINARY_NAME)
rm -f coverage.out
docker-build:
docker build \
--build-arg VERSION=$(VERSION) \
--build-arg BUILD_DATE=$(BUILD_DATE) \
--build-arg VCS_REF=$(VCS_REF) \
-t $(BINARY_NAME):latest \
-t $(BINARY_NAME):$(VERSION) .

View File

@@ -62,18 +62,16 @@ A software distribution platform for assets built and hosted on Gitea. Built wit
### Installation
1. **Build the Frontend**:
We use [Taskfile](https://taskfile.dev/) to manage the project.
1. **Build Everything (WASM, Frontend, Backend)**:
```bash
cd frontend
pnpm install
pnpm build
cd ..
go-task all
```
2. **Build and Run the Backend**:
2. **Run the Application**:
```bash
go build -o software-station .
./software-station -t YOUR_TOKEN -s https://your-gitea-instance.com -ua-blocklist ua-blocklist.txt
go-task run
```
### Docker (Recommended)
@@ -107,23 +105,18 @@ The frontend uses Tailwind CSS. You can customize the look and feel in `frontend
## Development
Run the backend and frontend separately for a better development experience:
Run the backend and frontend simultaneously with live reload (uses parallel tasks):
```bash
# Backend (with live reload using Air or just go run)
go run main.go
# Frontend (Vite dev server)
cd frontend
pnpm dev
go-task dev
```
## Testing
We maintain a high test coverage (>60%). Run the test suite:
Run the full test suite (including WASM tests):
```bash
go test -v -coverpkg=./... ./...
go-task test
```
## License

160
Taskfile.yml Normal file
View File

@@ -0,0 +1,160 @@
version: '3'
vars:
BINARY_NAME: software-station
FRONTEND_DIR: frontend
BUILD_DIR: build
VERIFIER_DIR: software-verifier
WASM_OUT: frontend/static/verifier
VERSION:
sh: grep '"version":' frontend/package.json | cut -d'"' -f4
BUILD_DATE:
sh: date -u +'%Y-%m-%dT%H:%M:%SZ'
VCS_REF:
sh: git rev-parse --short HEAD 2>/dev/null || echo "unknown"
tasks:
default:
desc: Build everything
cmds:
- task: all
all:
desc: Build everything
deps: [build-go]
dev:
desc: Start development environment (parallel)
deps: [build-wasm]
cmds:
- task: dev-frontend
- task: dev-backend
parallel: true
dev-frontend:
internal: true
dir: "{{.FRONTEND_DIR}}"
cmds:
- pnpm dev
dev-backend:
internal: true
cmds:
- go run main.go
preview:
desc: Preview the production build
dir: "{{.FRONTEND_DIR}}"
cmds:
- pnpm preview
build-wasm:
desc: Build WASM verifier
sources:
- "{{.VERIFIER_DIR}}/**/*.go"
generates:
- "{{.WASM_OUT}}/verifier.wasm"
- "{{.WASM_OUT}}/wasm_exec.js"
cmds:
- mkdir -p {{.WASM_OUT}}
- cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" {{.WASM_OUT}}/wasm_exec.js
- cd {{.VERIFIER_DIR}} && GOOS=js GOARCH=wasm go build -o ../{{.WASM_OUT}}/verifier.wasm .
- go run scripts/sri-gen/main.go frontend/src/lib/verifier.ts
silent: true
build-frontend:
desc: Build Svelte frontend
deps: [build-wasm]
sources:
- "{{.FRONTEND_DIR}}/**/*"
- exclude: "{{.FRONTEND_DIR}}/node_modules/**/*"
- exclude: "{{.FRONTEND_DIR}}/build/**/*"
generates:
- "{{.FRONTEND_DIR}}/build/**/*"
cmds:
- cd {{.FRONTEND_DIR}} && pnpm install && pnpm build
- go run scripts/sri-gen/main.go
build-go:
desc: Build main Go application
deps: [build-frontend]
sources:
- "**/*.go"
- exclude: "{{.VERIFIER_DIR}}/**/*"
- exclude: "scripts/**/*"
generates:
- "{{.BINARY_NAME}}"
cmds:
- go build -o {{.BINARY_NAME}} main.go
release:
desc: Build release binary
deps: [build-frontend]
cmds:
- CGO_ENABLED=0 go build -ldflags="-s -w" -o {{.BINARY_NAME}} main.go
run:
desc: Run the application
deps: [all]
cmds:
- ./{{.BINARY_NAME}}
format:
desc: Format code
cmds:
- go fmt ./...
- cd {{.FRONTEND_DIR}} && pnpm run format
lint:
desc: Lint code
cmds:
- go vet ./...
- cd {{.FRONTEND_DIR}} && pnpm run lint
scan:
desc: Security scan
cmds:
- gosec ./...
check:
desc: Type check frontend
cmds:
- cd {{.FRONTEND_DIR}} && pnpm run check
tidy:
desc: Run format, lint, and check
cmds:
- task: format
- task: lint
- task: check
test:
desc: Run tests
deps: [test-wasm]
cmds:
- go test -v -coverpkg=./... ./...
test-wasm:
desc: Run WASM tests
dir: "{{.VERIFIER_DIR}}"
cmds:
- go test -v ./...
clean:
desc: Clean build artifacts
cmds:
- rm -rf {{.FRONTEND_DIR}}/build
- rm -rf {{.WASM_OUT}}
- rm -f {{.BINARY_NAME}}
- rm -f coverage.out
docker-build:
desc: Build Docker image
cmds:
- |
docker build \
--build-arg VERSION={{.VERSION}} \
--build-arg BUILD_DATE={{.BUILD_DATE}} \
--build-arg VCS_REF={{.VCS_REF}} \
-t {{.BINARY_NAME}}:latest \
-t {{.BINARY_NAME}}:{{.VERSION}} .

3
frontend/.gitignore vendored
View File

@@ -21,3 +21,6 @@ Thumbs.db
# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.taskfile.env
.task

View File

@@ -3,4 +3,5 @@ node_modules
build
dist
.DS_Store
static/verifier

View File

@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.4.0",
"version": "0.4.1",
"type": "module",
"scripts": {
"dev": "vite dev",
@@ -36,7 +36,7 @@
"svelte-check": "^4.3.5",
"svelte-eslint-parser": "^1.4.1",
"svelte-i18n": "^4.0.1",
"tailwindcss": "^3.4.19",
"tailwindcss": "^4.0.0",
"typescript": "^5.9.3",
"vite": "^7.3.0"
}

View File

@@ -400,7 +400,16 @@
</div>
<div class="text-center px-4">
<p class="font-bold text-lg leading-tight">Verification Failed</p>
<p class="text-xs text-muted-foreground mt-1">{errorMessage}</p>
{#if errorMessage.includes('(')}
<p class="text-sm font-bold mt-1">
{errorMessage.split(' (')[0]}
</p>
<p class="text-[10px] text-muted-foreground mt-0.5 italic">
({errorMessage.split(' (')[1]}
</p>
{:else}
<p class="text-xs text-muted-foreground mt-1">{errorMessage}</p>
{/if}
</div>
</div>

View File

@@ -12,14 +12,19 @@ export async function loadVerifier() {
script.integrity = 'sha384-PWCs+V4BDf9yY1yjkD/p+9xNEs4iEbuvq+HezAOJiY3XL5GI6VyJXMsvnjiwNbce';
script.crossOrigin = 'anonymous';
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load WASM executor script'));
script.onerror = (e) => {
console.error('WASM executor script load error:', e);
reject(new Error('Failed to load WASM executor script (SRI mismatch or network error)'));
};
document.head.appendChild(script);
});
}
const go = new (window as any).Go();
const result = await WebAssembly.instantiateStreaming(
fetch('/verifier/verifier.wasm'),
fetch('/verifier/verifier.wasm', {
integrity: 'sha384-r/ciHEFn1SsJLxB/24OIDDJAb/oBbYWq5Tp/WksIkL3Kcsspi27fO7Hak5nZi8j4',
}),
go.importObject
);
go.run(result.instance);

View File

Binary file not shown.

View File

@@ -2,30 +2,22 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
'use strict';
"use strict";
(() => {
const enosys = () => {
const err = new Error('not implemented');
err.code = 'ENOSYS';
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = '';
let outputBuf = "";
globalThis.fs = {
constants: {
O_WRONLY: -1,
O_RDWR: -1,
O_CREAT: -1,
O_TRUNC: -1,
O_APPEND: -1,
O_EXCL: -1,
O_DIRECTORY: -1,
}, // unused
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf('\n');
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
@@ -40,147 +32,81 @@
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) {
callback(enosys());
},
chown(path, uid, gid, callback) {
callback(enosys());
},
close(fd, callback) {
callback(enosys());
},
fchmod(fd, mode, callback) {
callback(enosys());
},
fchown(fd, uid, gid, callback) {
callback(enosys());
},
fstat(fd, callback) {
callback(enosys());
},
fsync(fd, callback) {
callback(null);
},
ftruncate(fd, length, callback) {
callback(enosys());
},
lchown(path, uid, gid, callback) {
callback(enosys());
},
link(path, link, callback) {
callback(enosys());
},
lstat(path, callback) {
callback(enosys());
},
mkdir(path, perm, callback) {
callback(enosys());
},
open(path, flags, mode, callback) {
callback(enosys());
},
read(fd, buffer, offset, length, position, callback) {
callback(enosys());
},
readdir(path, callback) {
callback(enosys());
},
readlink(path, callback) {
callback(enosys());
},
rename(from, to, callback) {
callback(enosys());
},
rmdir(path, callback) {
callback(enosys());
},
stat(path, callback) {
callback(enosys());
},
symlink(path, link, callback) {
callback(enosys());
},
truncate(path, length, callback) {
callback(enosys());
},
unlink(path, callback) {
callback(enosys());
},
utimes(path, atime, mtime, callback) {
callback(enosys());
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() {
return -1;
},
getgid() {
return -1;
},
geteuid() {
return -1;
},
getegid() {
return -1;
},
getgroups() {
throw enosys();
},
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() {
throw enosys();
},
cwd() {
throw enosys();
},
chdir() {
throw enosys();
},
};
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.path) {
globalThis.path = {
resolve(...pathSegments) {
return pathSegments.join('/');
},
};
return pathSegments.join("/");
}
}
}
if (!globalThis.crypto) {
throw new Error(
'globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)'
);
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error(
'globalThis.performance is not available, polyfill required (performance.now only)'
);
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error('globalThis.TextEncoder is not available, polyfill required');
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error('globalThis.TextDecoder is not available, polyfill required');
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder('utf-8');
const decoder = new TextDecoder('utf-8');
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ['js'];
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn('exit code:', code);
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
@@ -193,17 +119,17 @@
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
};
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
};
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
};
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
@@ -216,12 +142,12 @@
const id = this.mem.getUint32(addr, true);
return this._values[id];
};
}
const storeValue = (addr, v) => {
const nanHead = 0x7ff80000;
const nanHead = 0x7FF80000;
if (typeof v === 'number' && v !== 0) {
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
@@ -249,30 +175,30 @@
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case 'object':
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case 'string':
case "string":
typeFlag = 2;
break;
case 'symbol':
case "symbol":
typeFlag = 3;
break;
case 'function':
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
};
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
};
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
@@ -282,18 +208,18 @@
a[i] = loadValue(array + i * 8);
}
return a;
};
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
};
}
const testCallExport = (a, b) => {
this._inst.exports.testExport0();
return this._inst.exports.testExport(a, b);
};
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
@@ -308,7 +234,7 @@
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
'runtime.wasmExit': (sp) => {
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
@@ -321,7 +247,7 @@
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
'runtime.wasmWrite': (sp) => {
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
@@ -330,50 +256,47 @@
},
// func resetMemoryDataView()
'runtime.resetMemoryDataView': (sp) => {
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
'runtime.nanotime1': (sp) => {
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
'runtime.walltime': (sp) => {
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = new Date().getTime();
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
'runtime.scheduleTimeoutEvent': (sp) => {
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(
id,
setTimeout(
() => {
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn('scheduleTimeoutEvent: missed timeout event');
this._resume();
}
},
getInt64(sp + 8)
)
);
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
'runtime.clearTimeoutEvent': (sp) => {
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
@@ -381,13 +304,13 @@
},
// func getRandomData(r []byte)
'runtime.getRandomData': (sp) => {
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
'syscall/js.finalizeRef': (sp) => {
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
@@ -400,13 +323,13 @@
},
// func stringVal(value string) ref
'syscall/js.stringVal': (sp) => {
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
'syscall/js.valueGet': (sp) => {
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
@@ -414,31 +337,31 @@
},
// func valueSet(v ref, p string, x ref)
'syscall/js.valueSet': (sp) => {
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
'syscall/js.valueDelete': (sp) => {
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
'syscall/js.valueIndex': (sp) => {
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
'syscall/js.valueSetIndex': (sp) => {
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
'syscall/js.valueCall': (sp) => {
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
@@ -456,7 +379,7 @@
},
// func valueInvoke(v ref, args []ref) (ref, bool)
'syscall/js.valueInvoke': (sp) => {
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
@@ -473,7 +396,7 @@
},
// func valueNew(v ref, args []ref) (ref, bool)
'syscall/js.valueNew': (sp) => {
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
@@ -490,13 +413,13 @@
},
// func valueLength(v ref) int
'syscall/js.valueLength': (sp) => {
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
'syscall/js.valuePrepareString': (sp) => {
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
@@ -504,20 +427,20 @@
},
// valueLoadString(v ref, b []byte)
'syscall/js.valueLoadString': (sp) => {
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
'syscall/js.valueInstanceOf': (sp) => {
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, loadValue(sp + 8) instanceof loadValue(sp + 16) ? 1 : 0);
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
'syscall/js.copyBytesToGo': (sp) => {
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
@@ -532,7 +455,7 @@
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
'syscall/js.copyBytesToJS': (sp) => {
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
@@ -546,21 +469,20 @@
this.mem.setUint8(sp + 48, 1);
},
debug: (value) => {
"debug": (value) => {
console.log(value);
},
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error('Go.run: WebAssembly.Instance expected');
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [
// JS values that Go currently has references to, indexed by reference id
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
@@ -570,8 +492,7 @@
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([
// mapping from JS values to reference ids
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
@@ -579,7 +500,7 @@
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
@@ -587,7 +508,7 @@
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + '\0');
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
@@ -621,7 +542,7 @@
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error('total length of command line and environment variables exceeds limit');
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
@@ -633,7 +554,7 @@
_resume() {
if (this.exited) {
throw new Error('Go program has already exited');
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
@@ -650,5 +571,5 @@
return event.result;
};
}
};
}
})();

View File

@@ -25,9 +25,9 @@ const (
HeavyDownloaderLimit = rate.Limit(256 * KB) // 256KB/s
// Rate Limiting
GlobalRateLimit = 100
GlobalRateLimit = 2000
GlobalRateWindow = 1 * time.Minute
APIRateLimit = 30
APIRateLimit = 500
APIRateWindow = 1 * time.Minute
)

View File

@@ -1,3 +1,48 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"regexManagers": [
{
"fileMatch": ["^\\.gitea/workflows/.*\\.yml$"],
"matchStrings": [
"uses: https://git\\.quad4\\.io/actions/(?<packageName>checkout|setup-go|setup-node)@(?<currentDigest>[a-f0-9]{40}) # (?<currentValue>v[0-9]+(\\.[0-9]+)*)"
],
"depNameTemplate": "actions/{{{packageName}}}",
"datasourceTemplate": "github-tags"
},
{
"fileMatch": ["^\\.gitea/workflows/.*\\.yml$"],
"matchStrings": [
"uses: https://git\\.quad4\\.io/actions/setup-pnpm@(?<currentDigest>[a-f0-9]{40}) # (?<currentValue>v[0-9]+(\\.[0-9]+)*)"
],
"depNameTemplate": "pnpm/action-setup",
"datasourceTemplate": "github-tags"
},
{
"fileMatch": ["^\\.gitea/workflows/.*\\.yml$"],
"matchStrings": [
"uses: https://git\\.quad4\\.io/actions/setup-task@(?<currentDigest>[a-f0-9]{40}) # (?<currentValue>v[0-9]+(\\.[0-9]+)*)"
],
"depNameTemplate": "arduino/setup-task",
"datasourceTemplate": "github-tags"
},
{
"fileMatch": ["^\\.gitea/workflows/.*\\.yml$"],
"matchStrings": [
"uses: https://git\\.quad4\\.io/actions/gh-gomod-generate-sbom@(?<currentDigest>[a-f0-9]{40}) # (?<currentValue>v[0-9]+(\\.[0-9]+)*)"
],
"depNameTemplate": "CycloneDX/gh-gomod-generate-sbom",
"datasourceTemplate": "github-tags"
},
{
"fileMatch": ["^\\.gitea/workflows/.*\\.yml$"],
"matchStrings": [
"uses: https://git\\.quad4\\.io/actions/action-gh-release@(?<currentDigest>[a-f0-9]{40}) # (?<currentValue>v[0-9]+(\\.[0-9]+)*)"
],
"depNameTemplate": "softprops/action-gh-release",
"datasourceTemplate": "github-tags"
}
]
}

View File

@@ -12,23 +12,34 @@ import (
)
func main() {
buildDir := "frontend/build"
// Default behavior: process HTML in frontend/build
target := "frontend/build"
if len(os.Args) > 1 {
buildDir = os.Args[1]
target = os.Args[1]
}
fmt.Printf("Generating SRI hashes for assets in %s...\n", buildDir)
info, err := os.Stat(target)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
err := filepath.Walk(buildDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(path, ".html") {
return processHTML(path, buildDir)
}
return nil
})
if info.IsDir() {
fmt.Printf("Generating SRI hashes for assets in %s...\n", target)
err = filepath.Walk(target, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && strings.HasSuffix(path, ".html") {
return processHTML(path, target)
}
return nil
})
} else {
// If a single file is provided (like verifier.ts), process it specially
fmt.Printf("Updating SRI hashes in %s...\n", target)
err = processSourceFile(target)
}
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@@ -37,16 +48,12 @@ func main() {
}
func processHTML(htmlPath, buildDir string) error {
content, err := os.ReadFile(filepath.Clean(htmlPath)) // #nosec G304
content, err := os.ReadFile(filepath.Clean(htmlPath))
if err != nil {
return err
}
updated := string(content)
// Regex to find script and link tags that don't have integrity yet
// Matches: <script src="/_app/..."
// Matches: <link rel="stylesheet" href="/_app/..."
scriptRegex := regexp.MustCompile(`<(script|link)\s+([^>]*)(src|href)=["'](/[^"']+)["']([^>]*)>`)
matches := scriptRegex.FindAllStringSubmatch(updated, -1)
@@ -55,12 +62,10 @@ func processHTML(htmlPath, buildDir string) error {
attr := match[3]
url := match[4]
// Only process local assets starting with /
if !strings.HasPrefix(url, "/") || strings.HasPrefix(url, "//") {
continue
}
// Skip if integrity already exists
if strings.Contains(tag, "integrity=") {
continue
}
@@ -68,7 +73,6 @@ func processHTML(htmlPath, buildDir string) error {
filePath := filepath.Join(buildDir, url)
hash, err := calculateSHA384(filePath)
if err != nil {
// Asset might not exist (e.g. dynamic URL or external-ish local path)
fmt.Printf(" Skipping %s: %v\n", url, err)
continue
}
@@ -80,14 +84,58 @@ func processHTML(htmlPath, buildDir string) error {
}
if updated != string(content) {
return os.WriteFile(filepath.Clean(htmlPath), []byte(updated), 0644) // #nosec G306
return os.WriteFile(filepath.Clean(htmlPath), []byte(updated), 0644)
}
return nil
}
func processSourceFile(sourcePath string) error {
content, err := os.ReadFile(filepath.Clean(sourcePath))
if err != nil {
return err
}
updated := string(content)
// We need a way to map the asset to the SRI. For verifier.ts, we know the assets.
assets := map[string]string{
"wasm_exec.js": "frontend/static/verifier/wasm_exec.js",
"verifier.wasm": "frontend/static/verifier/verifier.wasm",
}
for assetName, assetPath := range assets {
hash, err := calculateSHA384(assetPath)
if err != nil {
fmt.Printf(" Warning: could not calculate hash for %s: %v\n", assetName, err)
continue
}
// Find the line that mentions the asset and update the NEXT integrity string
assetEscaped := strings.ReplaceAll(assetName, ".", "\\.")
assetPattern := regexp.MustCompile(`['"](/verifier/)?` + assetEscaped + `['"][\s\S]*?sha384-([^'"]+)`)
matches := assetPattern.FindAllStringSubmatchIndex(updated, -1)
// Process from end to start to not mess up indices
for i := len(matches) - 1; i >= 0; i-- {
match := matches[i]
oldHashStart, oldHashEnd := match[4], match[5]
oldHash := updated[oldHashStart:oldHashEnd]
if oldHash != hash {
updated = updated[:oldHashStart] + hash + updated[oldHashEnd:]
fmt.Printf(" Updated SRI for %s in %s\n", assetName, sourcePath)
}
}
}
if updated != string(content) {
return os.WriteFile(filepath.Clean(sourcePath), []byte(updated), 0644)
}
return nil
}
func calculateSHA384(path string) (string, error) {
f, err := os.Open(filepath.Clean(path)) // #nosec G304
f, err := os.Open(filepath.Clean(path))
if err != nil {
return "", err
}