fix(release): surface companion build and secure GitWorkshop deps
This commit is contained in:
+5
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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)"
|
||||
|
||||
Reference in New Issue
Block a user