fix(release): surface companion build and secure GitWorkshop deps

This commit is contained in:
archipelago
2026-09-11 06:10:59 -04:00
parent ef8c3a76be
commit dac29baf97
10 changed files with 1601 additions and 37 deletions
+5 -1
View File
@@ -2,6 +2,10 @@
## Unreleased
- **The Companion download advertises and re-announces the APK it actually serves.** The Discover banner and its install prompt now share the no-cache APK metadata, visibly report Companion 0.5.32 build 52, and remember dismissal per Android build rather than forever, so an existing browser gets one useful update prompt when the APK changes. The ISO gate reads the expected version from the Android build itself instead of accepting the stale 0.5.28 payload.
- **GitWorkshop's dependency audit is clean.** The pinned upstream client keeps its separately reviewable Archipelago integration patch and now applies a deterministic dependency patch: safe lock refreshes plus targeted `fflate`, React Router, and Vitest upgrades remove all ten production advisories and all eight development advisories. A clean install reports zero vulnerabilities; type-check, all 152 upstream unit tests, and the exact Archipelago subpath build pass.
- **Every completed payment now gets the full Lightning-style receipt screen.** Cashu and Fedimint sends no longer leave the payment form open behind a token; wallet, QR-scan, Web5, and app-requested sends all replace their forms with the animated success state. Payment hashes, transaction IDs, ecash tokens/notes, mint details, and other useful references remain copyable in the receipt, and receive completions open the same distinct payment-success modal. Minibits claims retain a short-lived durable receipt so the visible modal still reports success when another dashboard or Companion context wins the claim-poll race, while concurrent watchers now share one bounded relay fetch instead of queueing several long polls.
- **TollGate provisioning closes the free-access path without taking over an admin network.** Confirmed upstream `TollGate-*` access points are moved from LAN onto the paid network, mint URLs are normalized consistently, and operators can set a validated Lightning payout address without replacing merchant keys or other revenue-share identities. Malformed existing identity data now stops provisioning safely instead of being overwritten.
@@ -40,7 +44,7 @@
- **The App Store now makes Archipelago's source an invitation to contribute.** GitWorkshop has its real upstream icon and source-focused description, plus a dedicated “Your node. Your source.” banner explaining that users can browse the code, clone with ngit, and send issues, patches, and reviews over Nostr.
- **Source now packages GitWorkshop instead of maintaining a separate Nostr Git interface.** The pinned upstream client runs read-only behind the authenticated app gate, launches at the dashboard's same origin under `/app/archipelago-source/`, and uses the node's consent-gated NIP-07 bridge. The upstream revision declares no license; Archipelago's owner accepted that redistribution risk without representing the client as licensed. Production publication still requires dependency-risk disposition and a tested canonical Archipelago NIP-34/GRASP announcement.
- **Source now packages GitWorkshop instead of maintaining a separate Nostr Git interface.** The pinned upstream client runs read-only behind the authenticated app gate, launches at the dashboard's same origin under `/app/archipelago-source/`, and uses the node's consent-gated NIP-07 bridge. The upstream revision declares no license; Archipelago's owner accepted that redistribution risk without representing the client as licensed. Production publication still requires a tested canonical Archipelago NIP-34/GRASP announcement.
- **Changing the node password now reports a wrong current password directly.** The backend was already rejecting the request before changing either the web or SSH password, but its error sanitizer replaced that safe, actionable explanation with “check server logs.” The real validation error now reaches the password dialog.
+5 -1
View File
@@ -15,7 +15,11 @@ RUN git init \
COPY gitworkshop-archipelago.patch /tmp/gitworkshop-archipelago.patch
RUN git apply --check /tmp/gitworkshop-archipelago.patch \
&& git apply /tmp/gitworkshop-archipelago.patch
RUN npm ci
COPY gitworkshop-dependencies.patch /tmp/gitworkshop-dependencies.patch
RUN git apply --check /tmp/gitworkshop-dependencies.patch \
&& git apply /tmp/gitworkshop-dependencies.patch
RUN npm ci \
&& npm audit --audit-level=moderate
RUN APP_BASE_PATH=/app/archipelago-source/ \
APP_RELEASE_VERSION="archipelago-${GITWORKSHOP_COMMIT}" \
npm run build
+13 -6
View File
@@ -8,12 +8,19 @@ This image packages the GitWorkshop NIP-34 web client from:
- App icon: `public/icons/icon.svg` from the same pinned revision (the artwork
is only inset onto Archipelago's standard icon safe area).
The small build patch only makes the upstream Vite/React application work
below Archipelago's `/app/archipelago-source/` mount, injects the existing
consent-gated Archipelago NIP-07 provider, disables the development-only
`localhost:4869` cache-relay probe, and removes two unreachable lookup relays
from the defaults. It does not replace GitWorkshop's NIP-34, GRASP, repository
browser, issue, pull-request, or review interfaces.
The Archipelago integration patch only makes the upstream Vite/React
application work below Archipelago's `/app/archipelago-source/` mount, injects
the existing consent-gated Archipelago NIP-07 provider, disables the
development-only `localhost:4869` cache-relay probe, and removes two unreachable
lookup relays from the defaults. It does not replace GitWorkshop's NIP-34,
GRASP, repository browser, issue, pull-request, or review interfaces.
The separate dependency patch refreshes the npm lockfile and moves `fflate` to
0.8.3, `react-router-dom` to 7.18.3, and Vitest to 5.0.0. The resulting clean
install reports zero npm advisories; its type-check, 152 unit tests, and
Archipelago subpath production build pass. Keeping this mechanical security
update separate makes both the upstream integration and future dependency
refreshes auditable.
The pinned revision and current upstream `main` do not contain a license file,
the package metadata declares no license, and GitHub reports no detected
File diff suppressed because it is too large Load Diff
+9 -8
View File
@@ -46,12 +46,19 @@ publication.
- The release-root signer exists locally as a binary, while its mnemonic
remains correctly offline.
- ISO dependencies (`xorriso`, `mksquashfs`, and QEMU) are installed.
- The corrected release gate passed on 2026-09-11: 1,091 frontend tests,
- The corrected release gate passed on 2026-09-11: 1,093 frontend tests,
production UI build, registry candidate/trust checks, Rust check, 114 focused
backend tests plus orchestration, and live dashboard/AIUI/RPC probes. Android
lint and the mobile Chromium signer-handoff regression also pass.
- GitWorkshop's refreshed dependency lock reports zero production or
development npm advisories. Its type-check, all 152 unit tests, and exact
Archipelago subpath production build pass with `fflate` 0.8.3, React Router
7.18.3, and Vitest 5.0.0.
- The locally served Companion candidate is 28,157,847 bytes with SHA-256
`30e0b6257f17bb5e072941ccce6fde04b6c8ae59a49b45169e1920aec1b108cf`.
- The Discover banner and automatic Companion prompt read that artifact's
no-cache metadata, display 0.5.32 build 52, and remember dismissal per build;
the ISO gate derives the same expected version from the Android project.
- The owner accepted the risk of redistributing the pinned, unlicensed
GitWorkshop revision on 2026-09-11. `NOTICE` and `UPSTREAM.md` disclose that
decision without claiming an upstream license.
@@ -70,10 +77,6 @@ publication.
reachable in an existing public Git branch. It provides upgrade continuity,
not production authenticity. A future production Companion must use the
`com.archipelago.app` package with a privately held release key.
- The pinned GitWorkshop revision has 4 high and 6 moderate production npm
advisories. The final image is static nginx, reducing exposure to Node-server
advisories, but browser dependencies remain reachable. Release requires an
owner accept/fix decision.
- The repository is large: this checkout has about 5.4 GiB of packed objects,
and the Gitea repository is about 5 GiB. Public GRASP replication must be
allowed enough time and verified rather than assumed complete.
@@ -93,9 +96,7 @@ committed, placed in shell history, or stored on the node:
nsec on a command line.
2. The 24-word Archipelago release-master mnemonic for three signing actions:
the OTA manifest, the app catalog, and the ISO checksum document.
3. A decision to accept the recorded GitWorkshop dependency advisories for
1.8.12-alpha or pause for a dependency update.
4. A decision to publish 0.5.32 on the existing explicitly non-production
3. A decision to publish 0.5.32 on the existing explicitly non-production
`.debug` Companion channel, omit it, or pause for a secure production-app
signing ceremony.
@@ -143,6 +143,11 @@ import { ref, onMounted, onUnmounted, watch } from 'vue'
import * as QRCode from 'qrcode'
import { IS_DEMO, DEMO_PASSWORD } from '@/composables/useDemoIntro'
import { companionIntroRequested } from '@/composables/useCompanionIntro'
import {
companionRelease,
companionReleaseMarker,
loadCompanionRelease,
} from '@/composables/useCompanionRelease'
import { isCompanionApp } from '@/utils/openExternal'
import { useLoginTransitionStore } from '@/stores/loginTransition'
import { useServerStore } from '@/stores/server'
@@ -163,17 +168,7 @@ const DEFAULT_DOWNLOAD_URL = IS_DEMO
// metadata (ships in the frontend beside the APK at /packages/), written by
// publish-companion-apk.sh from the same gradle config that built the APK.
// Best-effort: no file, no note.
const companionVersion = ref<{ versionName: string; versionCode: number } | null>(null)
async function loadCompanionVersion() {
try {
const res = await fetch('/packages/archipelago-companion.json', { cache: 'no-store' })
if (!res.ok) return
const meta = await res.json()
if (meta && typeof meta.versionName === 'string' && meta.versionName) {
companionVersion.value = { versionName: meta.versionName, versionCode: Number(meta.versionCode) || 0 }
}
} catch { /* metadata is a nicety — the download works without it */ }
}
const companionVersion = companionRelease
// Deep-link scheme the companion app registers; carries the server entry the
// app should create (see docs/companion-pairing-qr.md for the contract).
@@ -213,10 +208,13 @@ let calmTicker: ReturnType<typeof setInterval> | null = null
// it. Server management for connected companions lives in the NESMenu instead.
const IN_COMPANION_APP = isCompanionApp()
onMounted(() => {
onMounted(async () => {
if (IN_COMPANION_APP) return
// The prompt is remembered per APK build, not forever. A browser that saw
// 0.5.28 should be told once when this node begins serving 0.5.32.
await loadCompanionRelease()
try {
if (localStorage.getItem(STORAGE_KEY) !== '1') {
if (localStorage.getItem(STORAGE_KEY) !== companionReleaseMarker(companionVersion.value)) {
setTimeout(maybeShow, BASE_DELAY_MS)
}
} catch {
@@ -267,7 +265,7 @@ watch(companionIntroRequested, (requested) => {
watch(visible, async (isVisible) => {
if (!isVisible) return
if (!companionVersion.value) void loadCompanionVersion()
if (!companionVersion.value) void loadCompanionRelease()
// Generate large and let CSS scale down — at 112px source a ~45-module QR
// is 2.5px/module, which camera decoders (the companion app included)
// routinely fail on. 512px keeps every module crisp.
@@ -454,7 +452,7 @@ function dismiss() {
visible.value = false
step.value = 'download'
try {
localStorage.setItem(STORAGE_KEY, '1')
localStorage.setItem(STORAGE_KEY, companionReleaseMarker(companionVersion.value))
} catch {
// ignore
}
@@ -0,0 +1,42 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
companionRelease,
companionReleaseMarker,
loadCompanionRelease,
} from '../useCompanionRelease'
beforeEach(() => {
companionRelease.value = null
})
afterEach(() => {
vi.unstubAllGlobals()
})
describe('companion release metadata', () => {
it('uses the Android build number as the durable prompt marker', () => {
expect(companionReleaseMarker({ versionName: '0.5.32', versionCode: 52 })).toBe('build:52')
expect(companionReleaseMarker({ versionName: '0.5.32', versionCode: 0 })).toBe(
'version:0.5.32',
)
expect(companionReleaseMarker(null)).toBe('1')
})
it('loads the same no-cache metadata used by the download prompt', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ versionName: '0.5.32', versionCode: 52 }),
})
vi.stubGlobal('fetch', fetchMock)
await expect(loadCompanionRelease()).resolves.toEqual({
versionName: '0.5.32',
versionCode: 52,
})
expect(companionRelease.value).toEqual({ versionName: '0.5.32', versionCode: 52 })
expect(fetchMock).toHaveBeenCalledWith('/packages/archipelago-companion.json', {
cache: 'no-store',
})
})
})
@@ -0,0 +1,51 @@
import { ref } from "vue";
export interface CompanionRelease {
versionName: string;
versionCode: number;
}
// One shared value keeps the Discover banner and the automatic/manual
// Companion prompt on exactly the same release metadata.
export const companionRelease = ref<CompanionRelease | null>(null);
let releaseRequest: Promise<CompanionRelease | null> | null = null;
export function loadCompanionRelease(): Promise<CompanionRelease | null> {
if (companionRelease.value) return Promise.resolve(companionRelease.value);
if (releaseRequest) return releaseRequest;
releaseRequest = fetch("/packages/archipelago-companion.json", {
cache: "no-store",
})
.then(async (response) => {
if (!response.ok) return null;
const metadata = await response.json();
if (
!metadata ||
typeof metadata.versionName !== "string" ||
!metadata.versionName
)
return null;
companionRelease.value = {
versionName: metadata.versionName,
versionCode: Number(metadata.versionCode) || 0,
};
return companionRelease.value;
})
.catch(() => null)
.finally(() => {
releaseRequest = null;
});
return releaseRequest;
}
export function companionReleaseMarker(
release: CompanionRelease | null,
): string {
if (release?.versionCode) return `build:${release.versionCode}`;
if (release?.versionName) return `version:${release.versionName}`;
return "1";
}
@@ -30,7 +30,9 @@
@click.stop="openCompanionIntro()"
class="glass-button rounded-lg px-6 py-2.5 text-sm font-medium"
>Install</button>
<span class="text-white/40 text-sm">Archipelago Companion · Android</span>
<span class="text-white/40 text-sm">
Archipelago Companion · Android<template v-if="companionRelease"> · v{{ companionRelease.versionName }} (build {{ companionRelease.versionCode }})</template>
</span>
</div>
</div>
@@ -44,11 +46,16 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { computed, onMounted } from 'vue'
import { isCompanionApp } from '@/utils/openExternal'
import { openCompanionIntro } from '@/composables/useCompanionIntro'
import { companionRelease, loadCompanionRelease } from '@/composables/useCompanionRelease'
const showPitch = computed(() => !isCompanionApp())
onMounted(() => {
if (showPitch.value) void loadCompanionRelease()
})
</script>
<style scoped>
+6 -4
View File
@@ -177,11 +177,13 @@ if [ -f "$WEB_UI/index.html" ]; then
COMPANION_META="$WEB_UI/packages/archipelago-companion.json"
COMPANION_APK="$WEB_UI/packages/archipelago-companion.apk"
if [ -s "$COMPANION_APK" ] && [ "$(jq -r '.versionName // empty' "$COMPANION_META" 2>/dev/null)" = "0.5.28" ] \
&& [ "$(jq -r '.versionCode // empty' "$COMPANION_META" 2>/dev/null)" = "48" ]; then
ok "Companion 0.5.28 (versionCode 48) APK included"
EXPECTED_COMPANION_NAME="$(sed -n 's/^[[:space:]]*versionName = "\(.*\)"/\1/p' "$REPO/Android/app/build.gradle.kts" | head -1)"
EXPECTED_COMPANION_CODE="$(sed -n 's/^[[:space:]]*versionCode = \([0-9]*\).*/\1/p' "$REPO/Android/app/build.gradle.kts" | head -1)"
if [ -s "$COMPANION_APK" ] && [ "$(jq -r '.versionName // empty' "$COMPANION_META" 2>/dev/null)" = "$EXPECTED_COMPANION_NAME" ] \
&& [ "$(jq -r '.versionCode // empty' "$COMPANION_META" 2>/dev/null)" = "$EXPECTED_COMPANION_CODE" ]; then
ok "Companion $EXPECTED_COMPANION_NAME (versionCode $EXPECTED_COMPANION_CODE) APK included"
else
bad "Companion 0.5.28 APK/metadata missing or stale"
bad "Companion APK/metadata missing or stale (expected $EXPECTED_COMPANION_NAME/$EXPECTED_COMPANION_CODE)"
fi
SETTINGS_BUNDLE="$(grep -rlF "v$EXPECTED_VERSION" "$WEB_UI/assets" 2>/dev/null | head -1)"