Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc94445ca0 | ||
|
|
04cf0f663a | ||
|
|
576c642da4 | ||
|
|
12866db84a | ||
|
|
a184254706 | ||
|
|
192e045426 | ||
|
|
9ac46a69f8 | ||
|
|
bf6ef9644c | ||
|
|
c32910809e |
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.8.8-alpha (2026-09-01)
|
||||||
|
|
||||||
|
- **SSH over the mesh is now a first-class setting.** Settings gains an "SSH over mesh" card: off by default, and when you allow it the node's mesh firewall opens port 22 — either to every mesh peer (behind an explicit "I understand" confirmation, because that's a real exposure) or only to the mesh addresses you list. The rule is owned by the node (the `90-ssh.nft` drop-in), so it survives upgrades and daemon reinstalls, and the card tells you up front whether sshd is running, whether it listens on IPv6 (the mesh is IPv6-only — this is what a broken attempt looks like before it happens), and whether password login is on (keys-only is the recommended pairing). From Termux on your phone, `fipssh <user>@<node-npub>` connects once the toggle is on — the npub is the durable address, and the command is shown with a copy button on the card.
|
||||||
|
|
||||||
|
- **The App Store now lists apps — not parts of apps.** The signed catalog carries every manifest because the node's update layer needs their pins, and the store briefly listed them all: Mempool API, LND UI, Bitcoin UI, the Pine voice engines, the IndeeHub and Immich backends, the mesh router and friends. Components are hidden from the store listing (they still appear where they belong — the Services tab of My Apps, once installed), and four entries that never earned a tile are gone outright: MorphOS server (old), the Web5 DID wallet, Lightning Stack (an untracked upstream bundle — LND covers the need), and CryptPad (never tested).
|
||||||
|
|
||||||
|
- **App icons now persist everywhere, in the proper container style.** Two fixes: installed apps render the icon from their own manifest — Cuprate no longer falls back to the generic A-mark on its Services tile — and the store grids (the Discover page) apply the same icon container treatment (backdrop, border, shadow) as My Apps, the detail pages, and Home. Manifest-declared UI apps also classify correctly again: Alby Hub installs into My Apps with a working tile, not into Services, because a probe miss no longer buries an app the manifest itself says has a frontend.
|
||||||
|
|
||||||
|
- **Installing from the store keeps you on the store page.** The install progress lives on the tile itself and the app appears in My Apps when it lands — no more being yanked to My Apps mid-browse.
|
||||||
|
|
||||||
## v1.8.7-alpha (2026-08-31)
|
## v1.8.7-alpha (2026-08-31)
|
||||||
|
|
||||||
- **What's New really does stop at v1.8.0 now.** The first correction removed old generated release blocks but missed six much older hand-written v1.2 sections at the bottom of the modal. Those sections are gone, and the release check now recognizes and rejects that legacy format too, so the history floor cannot falsely pass again.
|
- **What's New really does stop at v1.8.0 now.** The first correction removed old generated release blocks but missed six much older hand-written v1.2 sections at the bottom of the modal. Those sections are gone, and the release check now recognizes and rejects that legacy format too, so the history floor cannot falsely pass again.
|
||||||
|
|||||||
@@ -117,18 +117,6 @@
|
|||||||
"bitcoin-knots"
|
"bitcoin-knots"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "cryptpad",
|
|
||||||
"title": "CryptPad",
|
|
||||||
"version": "2024.12.0",
|
|
||||||
"description": "End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.",
|
|
||||||
"icon": "/assets/icon/favico-black-v2.svg",
|
|
||||||
"author": "XWiki SAS",
|
|
||||||
"category": "data",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0",
|
|
||||||
"repoUrl": "https://github.com/cryptpad/cryptpad"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "cuprate",
|
"id": "cuprate",
|
||||||
"title": "Cuprate",
|
"title": "Cuprate",
|
||||||
|
|||||||
@@ -28,10 +28,13 @@ app:
|
|||||||
network_policy: isolated
|
network_policy: isolated
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- host: 3000
|
- host: 3030
|
||||||
container: 3000
|
container: 3000
|
||||||
protocol: tcp
|
protocol: tcp
|
||||||
bind: 127.0.0.1
|
bind: 127.0.0.1
|
||||||
|
# 3030, not AdGuard Home's conventional 3000: Grafana owns :3000 on a
|
||||||
|
# node, and both being installable means the host ports must not
|
||||||
|
# collide (the orchestrator refuses/loads warn on overlap).
|
||||||
# open: the setup wizard and admin console carry AdGuard Home's own
|
# open: the setup wizard and admin console carry AdGuard Home's own
|
||||||
# login; the gate fronts the port (TLS, header fixes) without a
|
# login; the gate fronts the port (TLS, header fixes) without a
|
||||||
# second cookie challenge.
|
# second cookie challenge.
|
||||||
@@ -67,7 +70,7 @@ app:
|
|||||||
|
|
||||||
health_check:
|
health_check:
|
||||||
type: tcp
|
type: tcp
|
||||||
endpoint: localhost:3000
|
endpoint: localhost:3030
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -77,7 +80,7 @@ app:
|
|||||||
name: Admin console
|
name: Admin console
|
||||||
description: AdGuard Home web console
|
description: AdGuard Home web console
|
||||||
type: ui
|
type: ui
|
||||||
port: 3000
|
port: 3030
|
||||||
protocol: http
|
protocol: http
|
||||||
path: /
|
path: /
|
||||||
|
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
app:
|
|
||||||
id: cryptpad
|
|
||||||
name: CryptPad
|
|
||||||
version: 2024.12.0
|
|
||||||
upstream:
|
|
||||||
kind: github
|
|
||||||
repo: cryptpad/cryptpad
|
|
||||||
description: End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0
|
|
||||||
pull_policy: if-not-present
|
|
||||||
network: pasta
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- storage: 5Gi
|
|
||||||
|
|
||||||
resources:
|
|
||||||
memory_limit: 1Gi
|
|
||||||
disk_limit: 5Gi
|
|
||||||
|
|
||||||
security:
|
|
||||||
capabilities: []
|
|
||||||
readonly_root: false
|
|
||||||
no_new_privileges: true
|
|
||||||
network_policy: isolated
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- host: 3000
|
|
||||||
container: 3000
|
|
||||||
protocol: tcp
|
|
||||||
bind: 127.0.0.1
|
|
||||||
# gated: CryptPad is browser-only (its own per-user accounts sit on top
|
|
||||||
# of the node login, exactly like Vaultwarden), so the gate's session
|
|
||||||
# challenge costs nothing and keeps the pads behind the node login.
|
|
||||||
auth: gated
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- type: bind
|
|
||||||
source: /var/lib/archipelago/cryptpad
|
|
||||||
target: /cryptpad/data
|
|
||||||
options: [rw]
|
|
||||||
|
|
||||||
environment: []
|
|
||||||
|
|
||||||
health_check:
|
|
||||||
type: tcp
|
|
||||||
endpoint: localhost:3000
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
interfaces:
|
|
||||||
main:
|
|
||||||
name: CryptPad
|
|
||||||
description: Encrypted collaboration suite
|
|
||||||
type: ui
|
|
||||||
port: 3000
|
|
||||||
protocol: http
|
|
||||||
path: /
|
|
||||||
|
|
||||||
metadata:
|
|
||||||
author: XWiki SAS
|
|
||||||
category: data
|
|
||||||
icon: /assets/icon/favico-black-v2.svg
|
|
||||||
repo: https://github.com/cryptpad/cryptpad
|
|
||||||
tier: optional
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
*.log
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
README.md
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
FROM node:20-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Copy source code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM node:20-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy built application
|
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
|
||||||
COPY --from=builder /app/package.json ./
|
|
||||||
COPY --from=builder /app/public ./public
|
|
||||||
|
|
||||||
# Create non-root user
|
|
||||||
RUN addgroup -g 1000 appuser && \
|
|
||||||
adduser -D -u 1000 -G appuser appuser && \
|
|
||||||
mkdir -p /app/wallet && \
|
|
||||||
chown -R appuser:appuser /app
|
|
||||||
|
|
||||||
USER appuser
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
ENV WALLET_STORAGE=/app/wallet
|
|
||||||
ENV DWN_ENDPOINT=http://web5-dwn:3000
|
|
||||||
|
|
||||||
CMD ["node", "dist/index.js"]
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# DID Wallet
|
|
||||||
|
|
||||||
Web5 wallet with Decentralized Identifier (DID) support.
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From the apps directory
|
|
||||||
./build.sh did-wallet
|
|
||||||
|
|
||||||
# Or manually
|
|
||||||
cd did-wallet
|
|
||||||
docker build -t archipelago/did-wallet:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd did-wallet
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Ports
|
|
||||||
|
|
||||||
- **8083**: Web UI (dev: 18083)
|
|
||||||
|
|
||||||
## Running Locally
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -p 8083:8080 \
|
|
||||||
-v /tmp/archipelago-dev/did-wallet:/app/wallet \
|
|
||||||
-e DWN_ENDPOINT=http://localhost:13000 \
|
|
||||||
archipelago/did-wallet:latest
|
|
||||||
```
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
app:
|
|
||||||
id: did-wallet
|
|
||||||
name: Web5 DID Wallet
|
|
||||||
version: 1.0.0
|
|
||||||
# Built by this project — there is no upstream release feed to watch.
|
|
||||||
upstream:
|
|
||||||
kind: internal
|
|
||||||
description: Web5 wallet with Decentralized Identifier (DID) support. Manage your digital identity and Web5 assets.
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: archipelago/did-wallet:1.0.0
|
|
||||||
image_signature: cosign://...
|
|
||||||
pull_policy: if-not-present
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- storage: 2Gi
|
|
||||||
|
|
||||||
resources:
|
|
||||||
cpu_limit: 1
|
|
||||||
memory_limit: 512Mi
|
|
||||||
disk_limit: 2Gi
|
|
||||||
|
|
||||||
security:
|
|
||||||
capabilities: []
|
|
||||||
readonly_root: true
|
|
||||||
no_new_privileges: true
|
|
||||||
user: 1000
|
|
||||||
seccomp_profile: default
|
|
||||||
network_policy: isolated
|
|
||||||
apparmor_profile: did-wallet
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- host: 8088
|
|
||||||
container: 8080
|
|
||||||
protocol: tcp # Web UI
|
|
||||||
bind: 127.0.0.1
|
|
||||||
auth: gated
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- type: bind
|
|
||||||
source: /var/lib/archipelago/did-wallet
|
|
||||||
target: /app/wallet
|
|
||||||
options: [rw]
|
|
||||||
|
|
||||||
environment:
|
|
||||||
- WALLET_STORAGE=/app/wallet
|
|
||||||
|
|
||||||
health_check:
|
|
||||||
type: http
|
|
||||||
endpoint: http://127.0.0.1:8080
|
|
||||||
path: /health
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
web5_integration:
|
|
||||||
did_support: true
|
|
||||||
wallet_functionality: true
|
|
||||||
bitcoin_integration: true
|
|
||||||
Generated
-2747
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "did-wallet",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Web5 DID Wallet for Archipelago",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"start": "node dist/index.js",
|
|
||||||
"dev": "ts-node src/index.ts"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.18.2",
|
|
||||||
"@web5/api": "^0.9.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/express": "^4.17.21",
|
|
||||||
"@types/node": "^20.10.0",
|
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"ts-node": "^10.9.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>DID Wallet</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Web5 DID Wallet</h1>
|
|
||||||
<p>Decentralized Identity Wallet for Archipelago</p>
|
|
||||||
<div id="app">
|
|
||||||
<p>Wallet interface coming soon...</p>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const port = 8080;
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.static('public'));
|
|
||||||
|
|
||||||
// Health check endpoint
|
|
||||||
app.get('/health', (req, res) => {
|
|
||||||
res.json({ status: 'ok', service: 'did-wallet' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wallet API endpoints
|
|
||||||
app.get('/api/wallet/info', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
status: 'ok',
|
|
||||||
wallet: {
|
|
||||||
dids: [],
|
|
||||||
balance: 0
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/wallet/did/create', async (req, res) => {
|
|
||||||
// Placeholder for DID creation
|
|
||||||
res.json({
|
|
||||||
status: 'ok',
|
|
||||||
did: 'did:key:placeholder'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
app.listen(port, '0.0.0.0', () => {
|
|
||||||
console.log(`DID Wallet listening on port ${port}`);
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "commonjs",
|
|
||||||
"lib": ["ES2020"],
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Lightning Stack - uses official image
|
|
||||||
FROM lightninglabs/lightning-stack:v0.12.0
|
|
||||||
|
|
||||||
# Default configuration is in the image
|
|
||||||
# No additional setup needed
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
app:
|
|
||||||
id: lightning-stack
|
|
||||||
name: Lightning Stack
|
|
||||||
version: 0.12.0
|
|
||||||
# No public listing exists for lightninglabs/lightning-stack (checked
|
|
||||||
# docker.io, ghcr.io and github.com) — nothing can be queried automatically,
|
|
||||||
# so this one is tracked by hand.
|
|
||||||
upstream:
|
|
||||||
kind: manual
|
|
||||||
url: no public listing for lightninglabs/lightning-stack — verify by hand
|
|
||||||
description: Complete Lightning Network implementation. Includes LND, CLN, and management tools.
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: lightninglabs/lightning-stack:v0.12.0
|
|
||||||
image_signature: cosign://...
|
|
||||||
pull_policy: if-not-present
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- app_id: bitcoin-core
|
|
||||||
version: ">=24.0"
|
|
||||||
- storage: 50Gi
|
|
||||||
|
|
||||||
resources:
|
|
||||||
cpu_limit: 4
|
|
||||||
memory_limit: 4Gi
|
|
||||||
disk_limit: 50Gi
|
|
||||||
|
|
||||||
security:
|
|
||||||
capabilities: [NET_BIND_SERVICE]
|
|
||||||
readonly_root: true
|
|
||||||
no_new_privileges: true
|
|
||||||
user: 1000
|
|
||||||
seccomp_profile: default
|
|
||||||
network_policy: isolated
|
|
||||||
apparmor_profile: lightning-stack
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- host: 9738
|
|
||||||
container: 9735
|
|
||||||
protocol: tcp # P2P
|
|
||||||
auth: none
|
|
||||||
auth_rationale: >-
|
|
||||||
Lightning p2p. The BOLT-8 noise handshake authenticates and encrypts the channel itself.
|
|
||||||
- host: 10010
|
|
||||||
container: 10009
|
|
||||||
protocol: tcp # gRPC
|
|
||||||
auth: none
|
|
||||||
auth_rationale: >-
|
|
||||||
LND gRPC, authenticated by macaroon over TLS. Remote wallets depend on reaching this directly.
|
|
||||||
# Mirrors lnd's 18080 exemption — same LND REST API, same macaroon auth.
|
|
||||||
- host: 8091
|
|
||||||
container: 8080
|
|
||||||
protocol: tcp # REST/Web UI
|
|
||||||
auth: none
|
|
||||||
auth_rationale: >-
|
|
||||||
LND REST, authenticated by macaroon over TLS. A browser login page would break
|
|
||||||
Zeus and every non-browser wallet client, exactly as for lnd's 18080.
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- type: bind
|
|
||||||
source: /var/lib/archipelago/lightning-stack
|
|
||||||
target: /root/.lightning
|
|
||||||
options: [rw]
|
|
||||||
|
|
||||||
environment:
|
|
||||||
- BITCOIND_HOST=bitcoin-core
|
|
||||||
- BITCOIND_RPCUSER=${BITCOIN_RPC_USER}
|
|
||||||
- BITCOIND_RPCPASS=${BITCOIN_RPC_PASSWORD}
|
|
||||||
- NETWORK=mainnet
|
|
||||||
|
|
||||||
health_check:
|
|
||||||
type: http
|
|
||||||
endpoint: http://127.0.0.1:8080
|
|
||||||
path: /v1/getinfo
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
bitcoin_integration:
|
|
||||||
rpc_access: admin
|
|
||||||
sync_required: true
|
|
||||||
|
|
||||||
lightning_integration:
|
|
||||||
channel_management: true
|
|
||||||
payment_routing: true
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
node_modules
|
|
||||||
dist
|
|
||||||
*.log
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
README.md
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
FROM node:20-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy package files
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci --only=production
|
|
||||||
|
|
||||||
# Copy source code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build the application
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM node:20-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Copy built application
|
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
COPY --from=builder /app/node_modules ./node_modules
|
|
||||||
COPY --from=builder /app/package.json ./
|
|
||||||
|
|
||||||
# Create non-root user
|
|
||||||
RUN addgroup -g 1000 appuser && \
|
|
||||||
adduser -D -u 1000 -G appuser appuser && \
|
|
||||||
mkdir -p /app/data && \
|
|
||||||
chown -R appuser:appuser /app
|
|
||||||
|
|
||||||
USER appuser
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
ENV MORPHOS_DATA_DIR=/app/data
|
|
||||||
|
|
||||||
CMD ["node", "dist/index.js"]
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
app:
|
|
||||||
id: morphos-server
|
|
||||||
name: MorphOS Server
|
|
||||||
version: 1.0.0
|
|
||||||
# Built by this project — there is no upstream release feed to watch.
|
|
||||||
upstream:
|
|
||||||
kind: internal
|
|
||||||
description: MorphOS server platform. Decentralized application server.
|
|
||||||
|
|
||||||
container:
|
|
||||||
image: archipelago/morphos-server:1.0.0
|
|
||||||
image_signature: cosign://...
|
|
||||||
pull_policy: if-not-present
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- storage: 5Gi
|
|
||||||
|
|
||||||
resources:
|
|
||||||
cpu_limit: 2
|
|
||||||
memory_limit: 2Gi
|
|
||||||
disk_limit: 5Gi
|
|
||||||
|
|
||||||
security:
|
|
||||||
capabilities: []
|
|
||||||
readonly_root: true
|
|
||||||
no_new_privileges: true
|
|
||||||
user: 1000
|
|
||||||
seccomp_profile: default
|
|
||||||
network_policy: isolated
|
|
||||||
apparmor_profile: morphos-server
|
|
||||||
|
|
||||||
ports:
|
|
||||||
- host: 8089
|
|
||||||
container: 8080
|
|
||||||
protocol: tcp # Web UI
|
|
||||||
bind: 127.0.0.1
|
|
||||||
auth: gated
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- type: bind
|
|
||||||
source: /var/lib/archipelago/morphos-server
|
|
||||||
target: /app/data
|
|
||||||
options: [rw]
|
|
||||||
|
|
||||||
environment:
|
|
||||||
- MORPHOS_ENV=production
|
|
||||||
- MORPHOS_DATA_DIR=/app/data
|
|
||||||
|
|
||||||
health_check:
|
|
||||||
type: http
|
|
||||||
endpoint: http://127.0.0.1:8080
|
|
||||||
path: /health
|
|
||||||
interval: 30s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 3
|
|
||||||
Generated
-1161
File diff suppressed because it is too large
Load Diff
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "morphos-server",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "MorphOS server platform",
|
|
||||||
"main": "dist/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc",
|
|
||||||
"start": "node dist/index.js",
|
|
||||||
"dev": "ts-node src/index.ts"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"express": "^4.18.2"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/express": "^4.17.21",
|
|
||||||
"@types/node": "^20.10.0",
|
|
||||||
"typescript": "^5.3.3",
|
|
||||||
"ts-node": "^10.9.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const port = 8080;
|
|
||||||
|
|
||||||
// Middleware
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// Health check endpoint
|
|
||||||
app.get('/health', (req, res) => {
|
|
||||||
res.json({ status: 'ok', service: 'morphos-server', version: '1.0.0' });
|
|
||||||
});
|
|
||||||
|
|
||||||
// API endpoints
|
|
||||||
app.get('/api/info', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
name: 'MorphOS Server',
|
|
||||||
version: '1.0.0',
|
|
||||||
status: 'running'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start server
|
|
||||||
app.listen(port, '0.0.0.0', () => {
|
|
||||||
console.log(`MorphOS Server listening on port ${port}`);
|
|
||||||
console.log(`Data directory: ${process.env.MORPHOS_DATA_DIR || '/app/data'}`);
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "commonjs",
|
|
||||||
"lib": ["ES2020"],
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./src",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,8 @@ app:
|
|||||||
- storage: 50Gi
|
- storage: 50Gi
|
||||||
|
|
||||||
resources:
|
resources:
|
||||||
memory_limit: 0
|
# No memory limit: models are sized by the disk allowance below, and a
|
||||||
|
# RAM ceiling would just OOM-kill long inferences.
|
||||||
disk_limit: 50Gi
|
disk_limit: 50Gi
|
||||||
|
|
||||||
security:
|
security:
|
||||||
|
|||||||
Generated
+1
-1
@@ -104,7 +104,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.7-alpha"
|
version = "1.8.8-alpha"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"archipelago-container",
|
"archipelago-container",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "archipelago"
|
name = "archipelago"
|
||||||
version = "1.8.7-alpha"
|
version = "1.8.8-alpha"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description = "Archipelago Bitcoin Node OS - Native backend"
|
description = "Archipelago Bitcoin Node OS - Native backend"
|
||||||
|
|||||||
@@ -558,6 +558,11 @@ impl RpcHandler {
|
|||||||
self.handle_fips_remove_seed_anchor(&p).await
|
self.handle_fips_remove_seed_anchor(&p).await
|
||||||
}
|
}
|
||||||
"fips.apply-seed-anchors" => self.handle_fips_apply_seed_anchors().await,
|
"fips.apply-seed-anchors" => self.handle_fips_apply_seed_anchors().await,
|
||||||
|
"fips.ssh-over-mesh.get" => self.handle_fips_ssh_over_mesh_get().await,
|
||||||
|
"fips.ssh-over-mesh.set" => {
|
||||||
|
let p = params.unwrap_or(serde_json::json!({}));
|
||||||
|
self.handle_fips_ssh_over_mesh_set(&p).await
|
||||||
|
}
|
||||||
|
|
||||||
// System updates
|
// System updates
|
||||||
"update.check" => self.handle_update_check().await,
|
"update.check" => self.handle_update_check().await,
|
||||||
|
|||||||
@@ -261,4 +261,51 @@ impl RpcHandler {
|
|||||||
}).collect::<Vec<_>>(),
|
}).collect::<Vec<_>>(),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The SSH-over-mesh toggle state plus sshd preflights (the card explains
|
||||||
|
/// the rule instead of gating on it — see ssh_mesh.rs).
|
||||||
|
pub(super) async fn handle_fips_ssh_over_mesh_get(&self) -> Result<serde_json::Value> {
|
||||||
|
let state = fips::ssh_mesh::load(&self.config.data_dir).await;
|
||||||
|
let preflights = fips::ssh_mesh::preflights().await;
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"enabled": state.enabled,
|
||||||
|
"sources": state.sources,
|
||||||
|
"scope": if state.sources.is_empty() { "any" } else { "list" },
|
||||||
|
"preflights": preflights,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the toggle. Params: `{ enabled: bool, sources?: string[] }` —
|
||||||
|
/// an empty/absent source list opens port 22 to every mesh peer (the UI
|
||||||
|
/// confirms that explicitly before calling with it).
|
||||||
|
pub(super) async fn handle_fips_ssh_over_mesh_set(
|
||||||
|
&self,
|
||||||
|
params: &serde_json::Value,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let enabled = params
|
||||||
|
.get("enabled")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("missing boolean 'enabled'"))?;
|
||||||
|
let sources: Vec<String> = params
|
||||||
|
.get("sources")
|
||||||
|
.and_then(|v| v.as_array())
|
||||||
|
.map(|a| {
|
||||||
|
a.iter()
|
||||||
|
.filter_map(|s| s.as_str().map(str::to_string))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let (state, outcome) =
|
||||||
|
fips::ssh_mesh::set(&self.config.data_dir, enabled, &sources).await?;
|
||||||
|
let preflights = fips::ssh_mesh::preflights().await;
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"enabled": state.enabled,
|
||||||
|
"sources": state.sources,
|
||||||
|
"scope": if state.sources.is_empty() { "any" } else { "list" },
|
||||||
|
"applied": outcome.applied,
|
||||||
|
"removed": outcome.removed,
|
||||||
|
"reloaded": outcome.reloaded,
|
||||||
|
"preflights": preflights,
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,6 +141,12 @@ impl DockerPackageScanner {
|
|||||||
|
|
||||||
// Get metadata for this app
|
// Get metadata for this app
|
||||||
let metadata = get_app_metadata(&app_id);
|
let metadata = get_app_metadata(&app_id);
|
||||||
|
// Manifest-owned metadata (icon) wins over the static table: the
|
||||||
|
// manifest is what the catalog signed and what the App Store shows,
|
||||||
|
// so it is also what an installed tile must render.
|
||||||
|
let manifest_icon = real_manifest_metadata(&app_id)
|
||||||
|
.and_then(|m| m.get("icon").and_then(|v| v.as_str()).map(str::to_string))
|
||||||
|
.filter(|s| !s.trim().is_empty());
|
||||||
|
|
||||||
// Resolve UI address: separate UI containers > static map > dynamic ports
|
// Resolve UI address: separate UI containers > static map > dynamic ports
|
||||||
let lan_address = if app_id == "netbird" {
|
let lan_address = if app_id == "netbird" {
|
||||||
@@ -191,7 +197,7 @@ impl DockerPackageScanner {
|
|||||||
static_files: StaticFiles {
|
static_files: StaticFiles {
|
||||||
license: "MIT".to_string(),
|
license: "MIT".to_string(),
|
||||||
instructions: metadata.description.clone(),
|
instructions: metadata.description.clone(),
|
||||||
icon: metadata.icon.clone(),
|
icon: manifest_icon.unwrap_or_else(|| metadata.icon.clone()),
|
||||||
},
|
},
|
||||||
manifest: Manifest {
|
manifest: Manifest {
|
||||||
id: app_id.clone(),
|
id: app_id.clone(),
|
||||||
@@ -211,28 +217,34 @@ impl DockerPackageScanner {
|
|||||||
author: Some("Archipelago".to_string()),
|
author: Some("Archipelago".to_string()),
|
||||||
website: lan_address.clone(),
|
website: lan_address.clone(),
|
||||||
tier: Some(metadata.tier.to_string()),
|
tier: Some(metadata.tier.to_string()),
|
||||||
interfaces: if lan_address.is_some() || tor_address.is_some() {
|
interfaces: {
|
||||||
// `ui` is no longer implied by a published port: a
|
// `ui` is no longer implied by a published port: a
|
||||||
// headless backend with an exposed port is a service,
|
// headless backend with an exposed port is a service,
|
||||||
// not a launchable app. ui_detection consults the
|
// not a launchable app. ui_detection consults the
|
||||||
// manifest declaration first, then HTTP-probes the
|
// manifest declaration first, then HTTP-probes the
|
||||||
// port. Addresses stay present either way so the
|
// port. A DECLARED UI classifies the app as launchable
|
||||||
// Services tab can still show where a backend lives.
|
// even when no reachable address was confirmed this
|
||||||
|
// scan — the launch button falls back to the static
|
||||||
|
// port map, and burying a manifest-declared UI app
|
||||||
|
// (Alby Hub) in Services because a probe missed was
|
||||||
|
// exactly the classification bug this fixes.
|
||||||
let has_ui = super::ui_detection::has_web_ui(
|
let has_ui = super::ui_detection::has_web_ui(
|
||||||
&app_id,
|
&app_id,
|
||||||
lan_address.as_deref(),
|
lan_address.as_deref(),
|
||||||
package_state == PackageState::Running,
|
package_state == PackageState::Running,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
Some(Interfaces {
|
if lan_address.is_some() || tor_address.is_some() || has_ui {
|
||||||
main: Some(MainInterface {
|
Some(Interfaces {
|
||||||
ui: has_ui.then(|| "true".to_string()),
|
main: Some(MainInterface {
|
||||||
tor_config: tor_address.clone(),
|
ui: has_ui.then(|| "true".to_string()),
|
||||||
lan_config: None,
|
tor_config: tor_address.clone(),
|
||||||
}),
|
lan_config: None,
|
||||||
})
|
}),
|
||||||
} else {
|
})
|
||||||
None
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
available_update,
|
available_update,
|
||||||
@@ -322,6 +334,47 @@ fn is_transient_podman_helper(app_id: &str, ports: &[String]) -> bool {
|
|||||||
&& right.chars().all(|c| c.is_ascii_lowercase())
|
&& right.chars().all(|c| c.is_ascii_lowercase())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Raw `metadata` block of an installed app's real manifest — catalog overlay
|
||||||
|
/// first (origin-wins), disk manifest as fallback. Kept as raw JSON because
|
||||||
|
/// the typed `AppManifest` deliberately does not model `metadata`, yet its
|
||||||
|
/// `icon` is what makes an installed app's tile render the right icon on
|
||||||
|
/// every surface (My Apps, Services, launcher, companion) instead of the
|
||||||
|
/// generic A-mark — the exact regression Cuprate exposed on install.
|
||||||
|
fn real_manifest_metadata(app_id: &str) -> Option<serde_json::Value> {
|
||||||
|
for (id, value) in crate::container::app_catalog::catalog_manifest_values() {
|
||||||
|
if id == app_id {
|
||||||
|
return value.get("app").and_then(|a| a.get("metadata")).cloned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
if let Ok(dir) = std::env::var("ARCHIPELAGO_DATA_DIR") {
|
||||||
|
candidates.push(
|
||||||
|
std::path::PathBuf::from(dir)
|
||||||
|
.join("../apps")
|
||||||
|
.join(app_id)
|
||||||
|
.join("manifest.yml"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
candidates.push(
|
||||||
|
std::path::PathBuf::from("/opt/archipelago/apps")
|
||||||
|
.join(app_id)
|
||||||
|
.join("manifest.yml"),
|
||||||
|
);
|
||||||
|
for path in candidates {
|
||||||
|
let Ok(content) = std::fs::read_to_string(&path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(value) = serde_yaml::from_str::<serde_json::Value>(&content) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let meta = value.get("app").and_then(|a| a.get("metadata")).cloned();
|
||||||
|
if meta.is_some() {
|
||||||
|
return meta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn get_app_metadata(app_id: &str) -> AppMetadata {
|
fn get_app_metadata(app_id: &str) -> AppMetadata {
|
||||||
let mut meta = match app_id {
|
let mut meta = match app_id {
|
||||||
"bitcoin-core" => AppMetadata {
|
"bitcoin-core" => AppMetadata {
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ fn image_var_for_app(app_id: &str) -> Option<&'static str> {
|
|||||||
"vaultwarden" => Some("VAULTWARDEN_IMAGE"),
|
"vaultwarden" => Some("VAULTWARDEN_IMAGE"),
|
||||||
"nextcloud" => Some("NEXTCLOUD_IMAGE"),
|
"nextcloud" => Some("NEXTCLOUD_IMAGE"),
|
||||||
"searxng" => Some("SEARXNG_IMAGE"),
|
"searxng" => Some("SEARXNG_IMAGE"),
|
||||||
"cryptpad" => Some("CRYPTPAD_IMAGE"),
|
|
||||||
"filebrowser" => Some("FILEBROWSER_IMAGE"),
|
"filebrowser" => Some("FILEBROWSER_IMAGE"),
|
||||||
"nginx-proxy-manager" => Some("NPM_IMAGE"),
|
"nginx-proxy-manager" => Some("NPM_IMAGE"),
|
||||||
"portainer" => Some("PORTAINER_IMAGE"),
|
"portainer" => Some("PORTAINER_IMAGE"),
|
||||||
|
|||||||
@@ -305,6 +305,14 @@ pub async fn install(identity_dir: &Path) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SSH-over-mesh rides every config install so the on-state survives
|
||||||
|
// upgrades, reconnects, and the startup self-heal (see ssh_mesh.rs —
|
||||||
|
// this module owns the 90-ssh.nft slot exclusively).
|
||||||
|
let ssh_data_dir = identity_dir.parent().unwrap_or(identity_dir);
|
||||||
|
if let Err(e) = super::ssh_mesh::reconcile(ssh_data_dir).await {
|
||||||
|
tracing::warn!("ssh-over-mesh reconcile after config install failed (non-fatal): {e:#}");
|
||||||
|
}
|
||||||
|
|
||||||
sudo_install_file(&src_key, DAEMON_KEY_PATH, "0600").await?;
|
sudo_install_file(&src_key, DAEMON_KEY_PATH, "0600").await?;
|
||||||
// Heal a legacy fips_key.pub that was written as bech32 npub text
|
// Heal a legacy fips_key.pub that was written as bech32 npub text
|
||||||
// (pre-fix identity::write_fips_key_from_seed did this). Upstream
|
// (pre-fix identity::write_fips_key_from_seed did this). Upstream
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ pub mod dial;
|
|||||||
pub mod endpoints;
|
pub mod endpoints;
|
||||||
pub mod iface;
|
pub mod iface;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
pub mod ssh_mesh;
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,492 @@
|
|||||||
|
//! SSH over the FIPS mesh — a first-class settings toggle.
|
||||||
|
//!
|
||||||
|
//! `fips0` is default-deny inbound: the hardening baseline (`/etc/fips/
|
||||||
|
//! fips.nft`) rejects un-allowlisted ports, and the daemon's own drop-ins
|
||||||
|
//! (`80-web-ui.nft`, `85-app-ports.nft`) do not include 22. That is correct
|
||||||
|
//! by default — but the user asked to be able to SSH their node from Termux
|
||||||
|
//! over the phone's FIPS mesh instead of keeping a second VPN around for it,
|
||||||
|
//! and the mesh path already works end-to-end (verified live: the connect
|
||||||
|
//! reaches fips0 and gets a RST from the node).
|
||||||
|
//!
|
||||||
|
//! This module owns the whole lifecycle of the `90-ssh.nft` drop-in, exactly
|
||||||
|
//! the way `config.rs` owns `80-web-ui.nft` — a hand-added rule and this
|
||||||
|
//! feature can never fight over the same slot:
|
||||||
|
//!
|
||||||
|
//! * toggle OFF → drop-in removed, port 22 refused again
|
||||||
|
//! * toggle ON → drop-in written on every toggle change AND on every
|
||||||
|
//! daemon config install (upgrade, reconnect, self-heal),
|
||||||
|
//! so the on-state survives reinstalls idempotently
|
||||||
|
//! * scope → "any" (every mesh peer — a real exposure, gated in the
|
||||||
|
//! UI behind an explicit confirmation) or an explicit list
|
||||||
|
//! of mesh addresses
|
||||||
|
//!
|
||||||
|
//! Nothing else is touched: `80-web-ui.nft` / `85-app-ports.nft` belong to
|
||||||
|
//! `config.rs`, and the sshd process itself is entirely the operator's.
|
||||||
|
|
||||||
|
use std::net::Ipv6Addr;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
/// On-disk state under the archipelago data dir. Absent file = disabled,
|
||||||
|
/// which is the safe default for every node that never touched the toggle.
|
||||||
|
const STATE_FILE: &str = "fips-ssh-over-mesh.json";
|
||||||
|
|
||||||
|
/// The drop-in slot this module owns. 90 sorts after the daemon's own
|
||||||
|
/// drop-ins (80/85) so a human reading the directory sees the deliberate
|
||||||
|
/// order; the include order does not change semantics for plain accepts.
|
||||||
|
pub const DROPIN_PATH: &str = "/etc/fips/fips.d/90-ssh.nft";
|
||||||
|
|
||||||
|
/// The hardening baseline this drop-in hangs off. Same file `config.rs`
|
||||||
|
/// reloads after its own drop-ins.
|
||||||
|
const FIPS_NFT: &str = "/etc/fips/fips.nft";
|
||||||
|
|
||||||
|
/// Persisted toggle state.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
pub struct SshMeshState {
|
||||||
|
/// Whether port 22 is allowed through the fips0 baseline at all.
|
||||||
|
#[serde(default)]
|
||||||
|
pub enabled: bool,
|
||||||
|
/// Mesh addresses (ULAs) the rule is restricted to. Empty = any mesh
|
||||||
|
/// peer. Kept as strings as-entered but validated as IPv6 on save.
|
||||||
|
#[serde(default)]
|
||||||
|
pub sources: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state_path(data_dir: &Path) -> std::path::PathBuf {
|
||||||
|
data_dir.join(STATE_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the persisted state. Missing file = disabled, no sources — never an
|
||||||
|
/// error, so a fresh node and a deleted file both mean "off".
|
||||||
|
pub async fn load(data_dir: &Path) -> SshMeshState {
|
||||||
|
match tokio::fs::read_to_string(state_path(data_dir)).await {
|
||||||
|
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
|
||||||
|
Err(_) => SshMeshState::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validate and normalise an operator-supplied source list. Every entry must
|
||||||
|
/// be a parseable IPv6 address (mesh addresses are full ULAs, not CIDRs) —
|
||||||
|
/// anything else is refused with the offending entry named, so a typo can
|
||||||
|
/// never silently narrow or widen the rule.
|
||||||
|
pub fn validate_sources(raw: &[String]) -> Result<Vec<String>> {
|
||||||
|
let mut out = Vec::with_capacity(raw.len());
|
||||||
|
for entry in raw {
|
||||||
|
let trimmed = entry.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let addr: Ipv6Addr = trimmed
|
||||||
|
.parse()
|
||||||
|
.with_context(|| format!("not a valid mesh (IPv6) address: {trimmed:?}"))?;
|
||||||
|
out.push(addr.to_string());
|
||||||
|
}
|
||||||
|
out.dedup();
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the nft drop-in for a state. The rule shape mirrors the interim
|
||||||
|
/// manual unblock from the field notes (`ip6 saddr <ula> tcp dport 22
|
||||||
|
/// accept`) — an unrestricted rule is the same statement without the saddr.
|
||||||
|
pub fn render_dropin(state: &SshMeshState) -> String {
|
||||||
|
let mut out = String::from(
|
||||||
|
"# Written by archipelago — SSH over mesh (Settings → SSH over mesh).\n\
|
||||||
|
# Allows sshd (port 22) through the fips0 default-deny inbound\n\
|
||||||
|
# baseline. Remove = refused again; never edit 80/85-* by hand.\n",
|
||||||
|
);
|
||||||
|
if state.sources.is_empty() {
|
||||||
|
out.push_str("tcp dport 22 accept\n");
|
||||||
|
} else {
|
||||||
|
out.push_str(&format!(
|
||||||
|
"ip6 saddr {{ {} }} tcp dport 22 accept\n",
|
||||||
|
state.sources.join(", ")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write or remove the drop-in to match the persisted state, then reload the
|
||||||
|
/// baseline so the change is live immediately. Returns whether a reload was
|
||||||
|
/// attempted and succeeded — a node without the hardening baseline has
|
||||||
|
/// nothing to reload (port 22 is governed by sshd and the host firewall
|
||||||
|
/// there), which is reported rather than treated as failure.
|
||||||
|
pub async fn reconcile(data_dir: &Path) -> Result<ReconcileOutcome> {
|
||||||
|
let state = load(data_dir).await;
|
||||||
|
|
||||||
|
if !state.enabled {
|
||||||
|
let removed = remove_dropin().await?;
|
||||||
|
let reloaded = reload_nft().await;
|
||||||
|
return Ok(ReconcileOutcome {
|
||||||
|
applied: false,
|
||||||
|
removed,
|
||||||
|
reloaded,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure /etc/fips/fips.d exists, exactly like config::install.
|
||||||
|
let out = Command::new("sudo")
|
||||||
|
.args(["install", "-d", "-m", "0755", "/etc/fips/fips.d"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("sudo install -d /etc/fips/fips.d")?;
|
||||||
|
if !out.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"sudo install -d /etc/fips/fips.d failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr).trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dropin = render_dropin(&state);
|
||||||
|
let stage = std::env::temp_dir().join(format!("fips-ssh-{}.nft", std::process::id()));
|
||||||
|
tokio::fs::write(&stage, &dropin)
|
||||||
|
.await
|
||||||
|
.context("stage ssh nft drop-in")?;
|
||||||
|
let install = Command::new("sudo")
|
||||||
|
.args(["install", "-m", "0644"])
|
||||||
|
.arg(&stage)
|
||||||
|
.arg(DROPIN_PATH)
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
let _ = tokio::fs::remove_file(&stage).await;
|
||||||
|
let install = install?;
|
||||||
|
if !install.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"install {} failed: {}",
|
||||||
|
DROPIN_PATH,
|
||||||
|
String::from_utf8_lossy(&install.stderr).trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let reloaded = reload_nft().await;
|
||||||
|
Ok(ReconcileOutcome {
|
||||||
|
applied: true,
|
||||||
|
removed: false,
|
||||||
|
reloaded,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct ReconcileOutcome {
|
||||||
|
/// The allow rule is in place.
|
||||||
|
pub applied: bool,
|
||||||
|
/// A previously-written drop-in was removed this call.
|
||||||
|
pub removed: bool,
|
||||||
|
/// The hardening baseline existed and `nft -f` succeeded.
|
||||||
|
pub reloaded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn remove_dropin() -> Result<bool> {
|
||||||
|
match tokio::fs::try_exists(DROPIN_PATH).await {
|
||||||
|
Ok(true) => {}
|
||||||
|
_ => return Ok(false),
|
||||||
|
}
|
||||||
|
let out = Command::new("sudo")
|
||||||
|
.args(["rm", "-f", DROPIN_PATH])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("sudo rm 90-ssh.nft")?;
|
||||||
|
if !out.status.success() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"removing {} failed: {}",
|
||||||
|
DROPIN_PATH,
|
||||||
|
String::from_utf8_lossy(&out.stderr).trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tracing::info!("ssh-over-mesh: drop-in removed — port 22 refused over fips0 again");
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload the hardening baseline. Best-effort in the same spirit as
|
||||||
|
/// `config.rs`: absent baseline (nothing to reload) → Ok(false); a failed
|
||||||
|
/// reload is Ok(false) with a warn, never an error — the drop-in is on disk
|
||||||
|
/// either way and the next daemon install reloads it.
|
||||||
|
async fn reload_nft() -> bool {
|
||||||
|
match tokio::fs::try_exists(FIPS_NFT).await {
|
||||||
|
Ok(true) => {}
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
|
match Command::new("sudo")
|
||||||
|
.args(["nft", "-f", FIPS_NFT])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(out) if out.status.success() => true,
|
||||||
|
Ok(out) => {
|
||||||
|
tracing::warn!(
|
||||||
|
"ssh-over-mesh: nft reload failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr).trim()
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("ssh-over-mesh: nft reload failed: {e}");
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persist new state and reconcile immediately. Validation happens here so
|
||||||
|
/// an invalid source list can never reach disk, and reconcile reads back
|
||||||
|
/// exactly what was saved.
|
||||||
|
pub async fn set(
|
||||||
|
data_dir: &Path,
|
||||||
|
enabled: bool,
|
||||||
|
sources: &[String],
|
||||||
|
) -> Result<(SshMeshState, ReconcileOutcome)> {
|
||||||
|
let state = SshMeshState {
|
||||||
|
enabled,
|
||||||
|
sources: validate_sources(sources)?,
|
||||||
|
};
|
||||||
|
tokio::fs::create_dir_all(data_dir)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("mkdir -p {}", data_dir.display()))?;
|
||||||
|
tokio::fs::write(state_path(data_dir), serde_json::to_string_pretty(&state)?)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("write {}", state_path(data_dir).display()))?;
|
||||||
|
let outcome = reconcile(data_dir).await?;
|
||||||
|
Ok((state, outcome))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Preflights surfaced in the settings card. None of these gate the toggle —
|
||||||
|
/// they explain it: writing the rule on a node whose sshd doesn't listen on
|
||||||
|
/// IPv6 simply has no effect until sshd does, and the card says so instead of
|
||||||
|
/// the user discovering it as a silent connection failure.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||||
|
pub struct SshPreflights {
|
||||||
|
/// ssh.service (or sshd.service) is active.
|
||||||
|
pub sshd_active: bool,
|
||||||
|
/// Something listens on :22 for IPv6 (`[::]:22` or a dual-stack `*:22`).
|
||||||
|
/// fips0 is IPv6-only, so a 0.0.0.0-bound sshd is unreachable over it.
|
||||||
|
pub sshd_ipv6_listen: bool,
|
||||||
|
/// sshd_config's PasswordAuthentication (last directive wins, includes
|
||||||
|
/// after the main file). None = not found / unreadable.
|
||||||
|
pub password_auth: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn preflights() -> SshPreflights {
|
||||||
|
SshPreflights {
|
||||||
|
sshd_active: sshd_active().await,
|
||||||
|
sshd_ipv6_listen: sshd_ipv6_listen().await,
|
||||||
|
password_auth: password_auth_enabled().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sshd_active() -> bool {
|
||||||
|
for unit in ["ssh", "sshd"] {
|
||||||
|
if let Ok(out) = Command::new("systemctl")
|
||||||
|
.args(["is-active", "--quiet", unit])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
if out.status.success() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sshd_ipv6_listen() -> bool {
|
||||||
|
let Ok(out) = Command::new("ss").args(["-H", "-tln"]).output().await else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let text = String::from_utf8_lossy(&out.stdout);
|
||||||
|
text.lines().any(|line| {
|
||||||
|
let mut cols = line.split_whitespace();
|
||||||
|
// -t -l: State Recv-Q Send-Q Local:Port Peer:Port → local is col 4.
|
||||||
|
let _state = cols.next();
|
||||||
|
let _recv = cols.next();
|
||||||
|
let _send = cols.next();
|
||||||
|
match cols.next() {
|
||||||
|
Some(local) => {
|
||||||
|
let port_ok = local.rsplit(':').next() == Some("22");
|
||||||
|
let v6 = local.starts_with("[::]") || local.starts_with('*');
|
||||||
|
port_ok && v6
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn password_auth_enabled() -> Option<bool> {
|
||||||
|
let mut directives: Vec<bool> = Vec::new();
|
||||||
|
if let Ok(main) = tokio::fs::read_to_string("/etc/ssh/sshd_config").await {
|
||||||
|
collect_password_auth(&main, &mut directives);
|
||||||
|
}
|
||||||
|
if let Ok(includes) = glob_sorted("/etc/ssh/sshd_config.d/*.conf").await {
|
||||||
|
for path in includes {
|
||||||
|
if let Ok(content) = tokio::fs::read_to_string(&path).await {
|
||||||
|
collect_password_auth(&content, &mut directives);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
directives.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_password_auth(content: &str, out: &mut Vec<bool>) {
|
||||||
|
for line in content.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if let Some(rest) = trimmed.strip_prefix("PasswordAuthentication") {
|
||||||
|
let rest = rest.trim_start();
|
||||||
|
let value = rest.split_whitespace().next().unwrap_or("");
|
||||||
|
if value.eq_ignore_ascii_case("yes") {
|
||||||
|
out.push(true);
|
||||||
|
} else if value.eq_ignore_ascii_case("no") {
|
||||||
|
out.push(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn glob_sorted(pattern: &str) -> Result<Vec<std::path::PathBuf>> {
|
||||||
|
let dir = std::path::Path::new(pattern)
|
||||||
|
.parent()
|
||||||
|
.unwrap_or_else(|| Path::new("/"));
|
||||||
|
let prefix = std::path::Path::new(pattern)
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.and_then(|n| n.split('.').next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let mut files: Vec<std::path::PathBuf> = Vec::new();
|
||||||
|
let mut entries = tokio::fs::read_dir(dir)
|
||||||
|
.await
|
||||||
|
.context("read sshd_config.d")?;
|
||||||
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
|
let name = entry.file_name();
|
||||||
|
let name = name.to_string_lossy();
|
||||||
|
if name.starts_with(&prefix) && name.ends_with(".conf") {
|
||||||
|
files.push(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.sort();
|
||||||
|
Ok(files)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn disabled_is_the_default_and_missing_file_is_not_an_error() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let state = tokio::runtime::Runtime::new()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(load(dir.path()));
|
||||||
|
assert!(!state.enabled);
|
||||||
|
assert!(state.sources.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn any_peer_dropin_is_an_unrestricted_accept() {
|
||||||
|
let state = SshMeshState {
|
||||||
|
enabled: true,
|
||||||
|
sources: vec![],
|
||||||
|
};
|
||||||
|
let out = render_dropin(&state);
|
||||||
|
assert!(out.contains("tcp dport 22 accept"));
|
||||||
|
assert!(!out.contains("ip6 saddr"), "no saddr restriction expected");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn source_list_dropin_restricts_to_those_addresses() {
|
||||||
|
let state = SshMeshState {
|
||||||
|
enabled: true,
|
||||||
|
sources: vec![
|
||||||
|
"fd68:496d:fe34:a06d:cf1:6e4:b6a4:3586".to_string(),
|
||||||
|
"fd79:1aa:b9e9:4c9f:1f80:5376:9385:1824".to_string(),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let out = render_dropin(&state);
|
||||||
|
assert!(out.contains("ip6 saddr { fd68:496d:fe34:a06d:cf1:6e4:b6a4:3586, fd79:1aa:b9e9:4c9f:1f80:5376:9385:1824 } tcp dport 22 accept"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sources_must_be_ipv6_and_are_normalised() {
|
||||||
|
let bad = validate_sources(&["192.168.1.5".to_string()]).unwrap_err();
|
||||||
|
assert!(bad.to_string().contains("192.168.1.5"));
|
||||||
|
|
||||||
|
let bad = validate_sources(&["not-an-address".to_string()]).unwrap_err();
|
||||||
|
assert!(bad.to_string().contains("not-an-address"));
|
||||||
|
|
||||||
|
// Uppercase/whitespace entries normalise to canonical lowercase.
|
||||||
|
let ok = validate_sources(&[
|
||||||
|
" FD68:496D:FE34:A06D:0CF1:06E4:B6A4:3586 ".to_string(),
|
||||||
|
"fd68:496d:fe34:a06d:cf1:6e4:b6a4:3586".to_string(),
|
||||||
|
String::new(),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
ok,
|
||||||
|
vec!["fd68:496d:fe34:a06d:cf1:6e4:b6a4:3586".to_string()]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn state_round_trips_through_disk() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let state = SshMeshState {
|
||||||
|
enabled: true,
|
||||||
|
sources: vec!["fd00::1".to_string()],
|
||||||
|
};
|
||||||
|
std::fs::write(
|
||||||
|
dir.path().join(STATE_FILE),
|
||||||
|
serde_json::to_string(&state).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let loaded = tokio::runtime::Runtime::new()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(load(dir.path()));
|
||||||
|
assert_eq!(loaded, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_validates_before_persisting() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
|
let err = rt
|
||||||
|
.block_on(set(dir.path(), true, &["bogus".to_string()]))
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(err.to_string().contains("bogus"));
|
||||||
|
// Nothing was persisted.
|
||||||
|
let state = rt.block_on(load(dir.path()));
|
||||||
|
assert!(!state.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preflight_parse_helpers_cover_the_directives() {
|
||||||
|
let mut directives = Vec::new();
|
||||||
|
collect_password_auth(
|
||||||
|
"# comment\nPasswordAuthentication yes\nMatch all\n PasswordAuthentication no\n",
|
||||||
|
&mut directives,
|
||||||
|
);
|
||||||
|
assert_eq!(directives, vec![true, false]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sshd_ipv6_listen_recognises_dual_stack_and_v6_only() {
|
||||||
|
assert!(line_listens("[::]:22"));
|
||||||
|
assert!(line_listens("*:22"));
|
||||||
|
assert!(!line_listens("0.0.0.0:22"));
|
||||||
|
assert!(!line_listens("[::]:80"));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn line_listens(local: &str) -> bool {
|
||||||
|
let line = format!("LISTEN 0 128 {local} 0.0.0.0:*");
|
||||||
|
let mut cols = line.split_whitespace();
|
||||||
|
cols.next();
|
||||||
|
cols.next();
|
||||||
|
cols.next();
|
||||||
|
match cols.next() {
|
||||||
|
Some(l) => {
|
||||||
|
let port_ok = l.rsplit(':').next() == Some("22");
|
||||||
|
let v6 = l.starts_with("[::]") || l.starts_with('*');
|
||||||
|
port_ok && v6
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.8.7-alpha",
|
"version": "1.8.8-alpha",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"version": "1.8.7-alpha",
|
"version": "1.8.8-alpha",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/bip39": "^2.2.0",
|
"@scure/bip39": "^2.2.0",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "neode-ui",
|
"name": "neode-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.8.7-alpha",
|
"version": "1.8.8-alpha",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "./start-dev.sh",
|
"start": "./start-dev.sh",
|
||||||
|
|||||||
@@ -117,18 +117,6 @@
|
|||||||
"bitcoin-knots"
|
"bitcoin-knots"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "cryptpad",
|
|
||||||
"title": "CryptPad",
|
|
||||||
"version": "2024.12.0",
|
|
||||||
"description": "End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.",
|
|
||||||
"icon": "/assets/icon/favico-black-v2.svg",
|
|
||||||
"author": "XWiki SAS",
|
|
||||||
"category": "data",
|
|
||||||
"tier": "optional",
|
|
||||||
"dockerImage": "source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0",
|
|
||||||
"repoUrl": "https://github.com/cryptpad/cryptpad"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "cuprate",
|
"id": "cuprate",
|
||||||
"title": "Cuprate",
|
"title": "Cuprate",
|
||||||
|
|||||||
@@ -515,8 +515,10 @@ async function installApp(app: MarketplaceApp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queueInstall(app)
|
queueInstall(app)
|
||||||
toast.info("Installing " + (app.title ?? app.id) + " - check My Apps")
|
// Stay on the store page: the tile itself shows install progress via the
|
||||||
router.push('/dashboard/apps').catch(() => {})
|
// global watcher, and a forced jump to My Apps yanked the user out of the
|
||||||
|
// page they were deliberately browsing.
|
||||||
|
toast.info("Installing " + (app.title ?? app.id) + " — it will appear in My Apps")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
|
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
|
||||||
@@ -540,8 +542,8 @@ async function installCommunityApp(app: MarketplaceApp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
queueInstall(app)
|
queueInstall(app)
|
||||||
toast.info("Installing " + (app.title ?? app.id) + " - check My Apps")
|
// Stay on the store page (see installApp).
|
||||||
router.push('/dashboard/apps').catch(() => {})
|
toast.info("Installing " + (app.title ?? app.id) + " — it will appear in My Apps")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const installParams: Record<string, unknown> = { id: app.id, dockerImage: app.dockerImage, version: app.version }
|
const installParams: Record<string, unknown> = { id: app.id, dockerImage: app.dockerImage, version: app.version }
|
||||||
|
|||||||
@@ -6,33 +6,17 @@ import { PackageState, type PackageDataEntry } from '@/types/api'
|
|||||||
import { matchPageScheme, resolveAppUrl } from '../appSession/appSessionConfig'
|
import { matchPageScheme, resolveAppUrl } from '../appSession/appSessionConfig'
|
||||||
import { portIsGateFronted } from '../discover/curatedApps'
|
import { portIsGateFronted } from '../discover/curatedApps'
|
||||||
import { isAutoTabApp } from '@/utils/autoTabApps'
|
import { isAutoTabApp } from '@/utils/autoTabApps'
|
||||||
|
import {
|
||||||
|
SERVICE_NAMES as SHARED_SERVICE_NAMES,
|
||||||
|
isServiceContainer as sharedIsServiceContainer,
|
||||||
|
} from './serviceNames'
|
||||||
|
|
||||||
export type AppsTab = 'apps' | 'websites' | 'services'
|
export type AppsTab = 'apps' | 'websites' | 'services'
|
||||||
|
|
||||||
// Service container name patterns (backend/infra, not user-facing)
|
// Re-exported for every existing caller — the canon moved to serviceNames.ts
|
||||||
export const SERVICE_NAMES = new Set([
|
// so the App Store's catalog merge can share it without a circular import.
|
||||||
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
export const SERVICE_NAMES = SHARED_SERVICE_NAMES
|
||||||
// Headless backends with no user-facing UI: the Fedimint ecash client daemon,
|
export const isServiceContainer = sharedIsServiceContainer
|
||||||
// the Nostr relay, and the Meshtastic LoRa daemon (its chat UI lives in the
|
|
||||||
// built-in Mesh tab) belong in Services, not My Apps.
|
|
||||||
'fedimint-clientd', 'nostr-rs-relay', 'meshtastic',
|
|
||||||
'immich_postgres', 'immich_redis',
|
|
||||||
// immich is now a manifest-driven stack (app_id-named, hyphen). The server is
|
|
||||||
// the launcher app; postgres/redis are backends → Services.
|
|
||||||
'immich-postgres', 'immich-redis',
|
|
||||||
'mysql-mempool', 'mempool-api', 'archy-mempool-web',
|
|
||||||
'archy-bitcoin-ui', 'archy-lnd-ui', 'archy-electrs-ui',
|
|
||||||
'bitcoin-ui', 'lnd-ui', 'electrs-ui',
|
|
||||||
'indeedhub-postgres', 'indeedhub-redis', 'indeedhub-minio',
|
|
||||||
'indeedhub-api', 'indeedhub-ffmpeg',
|
|
||||||
'indeedhub-relay', 'indeedhub-build_api_1', 'indeedhub-build_ffmpeg-worker_1',
|
|
||||||
'indeedhub-build_postgres_1', 'indeedhub-build_redis_1', 'indeedhub-build_minio_1',
|
|
||||||
'indeedhub-build_minio-init_1', 'indeedhub-build_relay_1',
|
|
||||||
// Pine voice-assistant stack: the two Wyoming engines are backends (STT/TTS)
|
|
||||||
// reached by Home Assistant over host.containers.internal — the user-facing
|
|
||||||
// card is "pine" (the setup/status launcher), so the engines go to Services.
|
|
||||||
'pine-whisper', 'pine-piper', 'pine-openwakeword',
|
|
||||||
])
|
|
||||||
|
|
||||||
const INTERNAL_TOOLING_NAMES = new Set([
|
const INTERNAL_TOOLING_NAMES = new Set([
|
||||||
'buildx_buildkit_default',
|
'buildx_buildkit_default',
|
||||||
@@ -43,16 +27,7 @@ export function isInternalToolingPackage(id: string, pkg?: PackageDataEntry): bo
|
|||||||
return INTERNAL_TOOLING_NAMES.has(id) || INTERNAL_TOOLING_NAMES.has(manifestId) || id.startsWith('buildx_buildkit') || manifestId.startsWith('buildx_buildkit')
|
return INTERNAL_TOOLING_NAMES.has(id) || INTERNAL_TOOLING_NAMES.has(manifestId) || id.startsWith('buildx_buildkit') || manifestId.startsWith('buildx_buildkit')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isServiceContainer(id: string): boolean {
|
// isServiceContainer now re-exports the shared canon (serviceNames.ts).
|
||||||
if (SERVICE_NAMES.has(id)) return true
|
|
||||||
if (id.startsWith('indeedhub-build_')) return true
|
|
||||||
if (id.startsWith('archy-')) return true
|
|
||||||
// Backend naming patterns that never carry a user-facing UI: databases and
|
|
||||||
// caches. Safe to classify by suffix (a database is never a launcher).
|
|
||||||
if (/-(db|postgres|postgresql|redis|valkey|mariadb|mysql|cache)$/.test(id)) return true
|
|
||||||
if (id.endsWith('_db')) return true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isServicePackage(id: string, pkg?: PackageDataEntry): boolean {
|
export function isServicePackage(id: string, pkg?: PackageDataEntry): boolean {
|
||||||
if (isServiceContainer(id)) return true
|
if (isServiceContainer(id)) return true
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* Canonical internal/service app-id classification — the shared source for
|
||||||
|
* every surface that must decide "is this a user-facing app or a component
|
||||||
|
* of one": the My Apps/Services tabs (appsConfig) and the App Store listing
|
||||||
|
* (the signed-catalog merge in discover/curatedApps must not publish
|
||||||
|
* components as standalone store apps).
|
||||||
|
*
|
||||||
|
* Keep this module dependency-free: it is imported from both the apps view
|
||||||
|
* and the catalog module (which must not import view code that reads the
|
||||||
|
* store — circulars).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Service container ids (backend/infra, not user-facing). Mirrors the
|
||||||
|
// SERVICE_NAMES set that used to live in appsConfig.ts verbatim.
|
||||||
|
export const SERVICE_NAMES = new Set([
|
||||||
|
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
||||||
|
// Headless backends with no user-facing UI: the Fedimint ecash client daemon,
|
||||||
|
// the Nostr relay, and the Meshtastic LoRa daemon (its chat UI lives in the
|
||||||
|
// built-in Mesh tab) belong in Services, not My Apps.
|
||||||
|
'fedimint-clientd', 'nostr-rs-relay', 'meshtastic',
|
||||||
|
'immich_postgres', 'immich_redis',
|
||||||
|
// immich is now a manifest-driven stack (app_id-named, hyphen). The server is
|
||||||
|
// the launcher app; postgres/redis are backends → Services.
|
||||||
|
'immich-postgres', 'immich-redis',
|
||||||
|
'mysql-mempool', 'mempool-api', 'archy-mempool-web',
|
||||||
|
'archy-bitcoin-ui', 'archy-lnd-ui', 'archy-electrs-ui',
|
||||||
|
'bitcoin-ui', 'lnd-ui', 'electrs-ui',
|
||||||
|
'indeedhub-postgres', 'indeedhub-redis', 'indeedhub-minio',
|
||||||
|
'indeedhub-api', 'indeedhub-ffmpeg',
|
||||||
|
'indeedhub-relay', 'indeedhub-build_api_1', 'indeedhub-build_ffmpeg-worker_1',
|
||||||
|
'indeedhub-build_postgres_1', 'indeedhub-build_redis_1', 'indeedhub-build_minio_1',
|
||||||
|
'indeedhub-build_minio-init_1', 'indeedhub-build_relay_1',
|
||||||
|
// Pine voice-assistant stack: the two Wyoming engines are backends (STT/TTS)
|
||||||
|
// reached by Home Assistant over host.containers.internal — the user-facing
|
||||||
|
// card is "pine" (the setup/status launcher), so the engines go to Services.
|
||||||
|
'pine-whisper', 'pine-piper', 'pine-openwakeword',
|
||||||
|
])
|
||||||
|
|
||||||
|
// Node-bundled internals that are real manifests (so they surface in the
|
||||||
|
// signed catalog's manifest list) but are not store apps: the assistant and
|
||||||
|
// the bundled relay/UIs ship with the node itself, the DWN is internal
|
||||||
|
// plumbing, and the mesh router + CLN are components of other surfaces.
|
||||||
|
export const NODE_INTERNAL_IDS = new Set([
|
||||||
|
'aiui', 'fips-ui', 'strfry', 'web5-dwn', 'router', 'core-lightning',
|
||||||
|
])
|
||||||
|
|
||||||
|
/** Is this id a component/backend rather than a standalone user app? */
|
||||||
|
export function isServiceContainer(id: string): boolean {
|
||||||
|
if (SERVICE_NAMES.has(id)) return true
|
||||||
|
if (id.startsWith('indeedhub-build_')) return true
|
||||||
|
if (id.startsWith('archy-')) return true
|
||||||
|
// Backend naming patterns that never carry a user-facing UI: databases and
|
||||||
|
// caches. Safe to classify by suffix (a database is never a launcher).
|
||||||
|
if (/-(db|postgres|postgresql|redis|valkey|mariadb|mysql|cache)$/.test(id)) return true
|
||||||
|
if (id.endsWith('_db')) return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Should this app id appear in the App Store listing at all? */
|
||||||
|
export function isStoreListedApp(id: string): boolean {
|
||||||
|
return !isServiceContainer(id) && !NODE_INTERNAL_IDS.has(id)
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
v-if="app.icon"
|
v-if="app.icon"
|
||||||
:src="app.icon"
|
:src="app.icon"
|
||||||
:alt="app.title"
|
:alt="app.title"
|
||||||
class="w-14 h-14 rounded-lg object-cover"
|
class="w-14 h-14 rounded-lg archy-app-icon"
|
||||||
@error="handleImageError"
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
<div v-else class="w-14 h-14 rounded-lg bg-white/10 flex items-center justify-center">
|
<div v-else class="w-14 h-14 rounded-lg bg-white/10 flex items-center justify-center">
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
v-if="app.icon"
|
v-if="app.icon"
|
||||||
:src="app.icon"
|
:src="app.icon"
|
||||||
:alt="app.title"
|
:alt="app.title"
|
||||||
class="w-20 h-20 rounded-xl object-cover flex-shrink-0"
|
class="w-20 h-20 rounded-xl archy-app-icon flex-shrink-0"
|
||||||
@error="handleImageError"
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { MarketplaceApp } from './types'
|
import type { MarketplaceApp } from './types'
|
||||||
|
import { isStoreListedApp } from '../apps/serviceNames'
|
||||||
|
|
||||||
const R = 'source.archipelago-foundation.org/lfg2025'
|
const R = 'source.archipelago-foundation.org/lfg2025'
|
||||||
|
|
||||||
@@ -48,10 +49,15 @@ export interface SignedAppEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Convert the signed catalog's keyed entries into store-listing apps.
|
/** Convert the signed catalog's keyed entries into store-listing apps.
|
||||||
* Pure — unit-tested against the live catalog's shape (Cuprate). */
|
* Pure — unit-tested against the live catalog's shape (Cuprate).
|
||||||
|
* Components and node internals never enter the listing: the signed catalog
|
||||||
|
* carries every manifest (including backends like mempool-api and the
|
||||||
|
* bundled UIs) because the node's update layer needs their pins, but the
|
||||||
|
* App Store publishes apps, not parts of apps. */
|
||||||
export function signedCatalogToApps(catalog: SignedAppCatalog): MarketplaceApp[] {
|
export function signedCatalogToApps(catalog: SignedAppCatalog): MarketplaceApp[] {
|
||||||
const out: MarketplaceApp[] = []
|
const out: MarketplaceApp[] = []
|
||||||
for (const [id, entry] of Object.entries(catalog.apps || {})) {
|
for (const [id, entry] of Object.entries(catalog.apps || {})) {
|
||||||
|
if (!isStoreListedApp(id)) continue
|
||||||
const app = entry.manifest?.app
|
const app = entry.manifest?.app
|
||||||
out.push({
|
out.push({
|
||||||
id,
|
id,
|
||||||
@@ -200,7 +206,6 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
{ id: 'grafana', title: 'Grafana', version: '10.2.0', description: 'Analytics and monitoring platform. Dashboards for your node metrics and system health.', icon: '/assets/img/app-icons/grafana.png', author: 'Grafana Labs', dockerImage: `${R}/grafana:10.2.0`, repoUrl: 'https://github.com/grafana/grafana' },
|
{ id: 'grafana', title: 'Grafana', version: '10.2.0', description: 'Analytics and monitoring platform. Dashboards for your node metrics and system health.', icon: '/assets/img/app-icons/grafana.png', author: 'Grafana Labs', dockerImage: `${R}/grafana:10.2.0`, repoUrl: 'https://github.com/grafana/grafana' },
|
||||||
{ id: 'searxng', title: 'SearXNG', version: '2024.1.0', description: 'Privacy-respecting metasearch engine. Search the internet without being tracked or profiled.', icon: '/assets/img/app-icons/searxng.png', author: 'SearXNG', dockerImage: `${R}/searxng:latest`, repoUrl: 'https://github.com/searxng/searxng' },
|
{ id: 'searxng', title: 'SearXNG', version: '2024.1.0', description: 'Privacy-respecting metasearch engine. Search the internet without being tracked or profiled.', icon: '/assets/img/app-icons/searxng.png', author: 'SearXNG', dockerImage: `${R}/searxng:latest`, repoUrl: 'https://github.com/searxng/searxng' },
|
||||||
{ id: 'ollama', title: 'Ollama', version: '0.5.4', description: 'Run AI models locally. Llama, Mistral, and more — on your hardware, completely private.', icon: '/assets/img/app-icons/ollama.png', author: 'Ollama', dockerImage: `${R}/ollama:latest`, repoUrl: 'https://github.com/ollama/ollama' },
|
{ id: 'ollama', title: 'Ollama', version: '0.5.4', description: 'Run AI models locally. Llama, Mistral, and more — on your hardware, completely private.', icon: '/assets/img/app-icons/ollama.png', author: 'Ollama', dockerImage: `${R}/ollama:latest`, repoUrl: 'https://github.com/ollama/ollama' },
|
||||||
{ id: 'cryptpad', title: 'CryptPad', version: '2024.12.0', description: 'End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.', icon: '/assets/icon/favico-black-v2.svg', author: 'XWiki SAS', dockerImage: `${R}/cryptpad:2024.12.0`, repoUrl: 'https://github.com/cryptpad/cryptpad' },
|
|
||||||
{ id: 'nextcloud', title: 'Nextcloud', version: '29', description: 'Your own private cloud. File sync, calendars, contacts — all on your hardware.', icon: '/assets/img/app-icons/nextcloud.webp', author: 'Nextcloud', dockerImage: `${R}/nextcloud:29`, repoUrl: 'https://github.com/nextcloud/server' },
|
{ id: 'nextcloud', title: 'Nextcloud', version: '29', description: 'Your own private cloud. File sync, calendars, contacts — all on your hardware.', icon: '/assets/img/app-icons/nextcloud.webp', author: 'Nextcloud', dockerImage: `${R}/nextcloud:29`, repoUrl: 'https://github.com/nextcloud/server' },
|
||||||
{ id: 'vaultwarden', title: 'Vaultwarden', version: '1.30.0', description: 'Self-hosted password vault. Bitwarden-compatible with zero-knowledge encryption.', icon: '/assets/img/app-icons/vaultwarden.webp', author: 'Vaultwarden', dockerImage: `${R}/vaultwarden:1.30.0-alpine`, repoUrl: 'https://github.com/dani-garcia/vaultwarden' },
|
{ id: 'vaultwarden', title: 'Vaultwarden', version: '1.30.0', description: 'Self-hosted password vault. Bitwarden-compatible with zero-knowledge encryption.', icon: '/assets/img/app-icons/vaultwarden.webp', author: 'Vaultwarden', dockerImage: `${R}/vaultwarden:1.30.0-alpine`, repoUrl: 'https://github.com/dani-garcia/vaultwarden' },
|
||||||
{ id: 'jellyfin', title: 'Jellyfin', version: '10.8.13', description: 'Free media server. Stream your movies, music, and photos to any device.', icon: '/assets/img/app-icons/jellyfin.webp', author: 'Jellyfin', dockerImage: `${R}/jellyfin:10.8.13`, repoUrl: 'https://github.com/jellyfin/jellyfin' },
|
{ id: 'jellyfin', title: 'Jellyfin', version: '10.8.13', description: 'Free media server. Stream your movies, music, and photos to any device.', icon: '/assets/img/app-icons/jellyfin.webp', author: 'Jellyfin', dockerImage: `${R}/jellyfin:10.8.13`, repoUrl: 'https://github.com/jellyfin/jellyfin' },
|
||||||
|
|||||||
@@ -247,17 +247,6 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
manifestUrl: undefined,
|
manifestUrl: undefined,
|
||||||
repoUrl: 'https://github.com/ollama/ollama'
|
repoUrl: 'https://github.com/ollama/ollama'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'cryptpad',
|
|
||||||
title: 'CryptPad',
|
|
||||||
version: '2024.12.0',
|
|
||||||
description: 'End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.',
|
|
||||||
icon: '/assets/icon/favico-black-v2.svg',
|
|
||||||
author: 'XWiki SAS',
|
|
||||||
dockerImage: `${REGISTRY}/cryptpad:2024.12.0`,
|
|
||||||
manifestUrl: undefined,
|
|
||||||
repoUrl: 'https://github.com/cryptpad/cryptpad'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'nextcloud',
|
id: 'nextcloud',
|
||||||
title: 'Nextcloud',
|
title: 'Nextcloud',
|
||||||
|
|||||||
@@ -362,6 +362,19 @@ init()
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||||
|
<!-- v1.8.8-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.8-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">September 1, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p><strong>SSH over the mesh is now a first-class setting.</strong> Settings gains an "SSH over mesh" card: off by default, and when you allow it the node's mesh firewall opens port 22 — either to every mesh peer (behind an explicit "I understand" confirmation, because that's a real exposure) or only to the mesh addresses you list. The rule is owned by the node (the 90-ssh.nft drop-in), so it survives upgrades and daemon reinstalls, and the card tells you up front whether sshd is running, whether it listens on IPv6 (the mesh is IPv6-only — this is what a broken attempt looks like before it happens), and whether password login is on (keys-only is the recommended pairing). From Termux on your phone, fipssh <user>@<node-npub> connects once the toggle is on — the npub is the durable address, and the command is shown with a copy button on the card.</p>
|
||||||
|
<p><strong>The App Store now lists apps — not parts of apps.</strong> The signed catalog carries every manifest because the node's update layer needs their pins, and the store briefly listed them all: Mempool API, LND UI, Bitcoin UI, the Pine voice engines, the IndeeHub and Immich backends, the mesh router and friends. Components are hidden from the store listing (they still appear where they belong — the Services tab of My Apps, once installed), and four entries that never earned a tile are gone outright: MorphOS server (old), the Web5 DID wallet, Lightning Stack (an untracked upstream bundle — LND covers the need), and CryptPad (never tested).</p>
|
||||||
|
<p><strong>App icons now persist everywhere, in the proper container style.</strong> Two fixes: installed apps render the icon from their own manifest — Cuprate no longer falls back to the generic A-mark on its Services tile — and the store grids (the Discover page) apply the same icon container treatment (backdrop, border, shadow) as My Apps, the detail pages, and Home. Manifest-declared UI apps also classify correctly again: Alby Hub installs into My Apps with a working tile, not into Services, because a probe miss no longer buries an app the manifest itself says has a frontend.</p>
|
||||||
|
<p><strong>Installing from the store keeps you on the store page.</strong> The install progress lives on the tile itself and the app appears in My Apps when it lands — no more being yanked to My Apps mid-browse.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- v1.8.7-alpha -->
|
<!-- v1.8.7-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
|
import { useToast } from '@/composables/useToast'
|
||||||
|
|
||||||
|
interface SshOverMeshStatus {
|
||||||
|
enabled: boolean
|
||||||
|
sources: string[]
|
||||||
|
scope: 'any' | 'list'
|
||||||
|
preflights?: {
|
||||||
|
sshd_active: boolean
|
||||||
|
sshd_ipv6_listen: boolean
|
||||||
|
password_auth: boolean | null
|
||||||
|
}
|
||||||
|
applied?: boolean
|
||||||
|
reloaded?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
const status = ref<SshOverMeshStatus | null>(null)
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref('')
|
||||||
|
const saving = ref(false)
|
||||||
|
const confirmAnyPeer = ref(false)
|
||||||
|
const pendingEnableAny = ref(false)
|
||||||
|
const sourcesText = ref('')
|
||||||
|
const fipsNpub = ref<string | null>(null)
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
loading.value = true
|
||||||
|
error.value = ''
|
||||||
|
try {
|
||||||
|
status.value = (await rpcClient.call({ method: 'fips.ssh-over-mesh.get' })) as SshOverMeshStatus
|
||||||
|
sourcesText.value = (status.value.sources || []).join('\n')
|
||||||
|
if (!fipsNpub.value) {
|
||||||
|
try {
|
||||||
|
const s = (await rpcClient.call<{ npub?: string }>({ method: 'fips.status', dedup: true, maxRetries: 1 }))
|
||||||
|
fipsNpub.value = s.npub ?? null
|
||||||
|
} catch { /* npub hint is optional */ }
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
error.value = e instanceof Error ? e.message : 'Failed to load SSH-over-mesh state'
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The unrestricted scope opens port 22 to every mesh peer — demand an
|
||||||
|
* explicit confirmation before applying it, like the other danger zones. */
|
||||||
|
async function toggle(enabled: boolean) {
|
||||||
|
const sources = enabled && status.value?.scope === 'list'
|
||||||
|
? sourcesText.value.split(/[\n,]+/).map(s => s.trim()).filter(Boolean)
|
||||||
|
: (status.value?.sources ?? [])
|
||||||
|
if (enabled && sources.length === 0) {
|
||||||
|
pendingEnableAny.value = true
|
||||||
|
confirmAnyPeer.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await apply(enabled, sources)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function apply(enabled: boolean, sources: string[]) {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
status.value = (await rpcClient.call({
|
||||||
|
method: 'fips.ssh-over-mesh.set',
|
||||||
|
params: { enabled, sources },
|
||||||
|
timeout: 30_000,
|
||||||
|
})) as SshOverMeshStatus
|
||||||
|
sourcesText.value = (status.value.sources || []).join('\n')
|
||||||
|
toast.success(enabled ? 'SSH over mesh allowed' : 'SSH over mesh blocked')
|
||||||
|
if (enabled && status.value.reloaded === false) {
|
||||||
|
toast.error('Rule saved, but the firewall reload failed — reconnect or check logs')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof Error ? e.message : 'Failed to apply')
|
||||||
|
} finally {
|
||||||
|
saving.value = false
|
||||||
|
pendingEnableAny.value = false
|
||||||
|
confirmAnyPeer.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFipssh() {
|
||||||
|
if (!fipsNpub.value) return
|
||||||
|
const cmd = `fipssh <user>@${fipsNpub.value}`
|
||||||
|
navigator.clipboard?.writeText(cmd).then(
|
||||||
|
() => toast.success('Copied: ' + cmd),
|
||||||
|
() => toast.error('Copy failed'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(refresh)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="glass-card px-6 py-6 mb-6">
|
||||||
|
<div class="flex items-start justify-between gap-4 mb-2">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-xl font-semibold text-white/96">SSH over mesh</h2>
|
||||||
|
<p class="text-sm text-white/60 mt-1">
|
||||||
|
Let the phone's FIPS mesh reach this node's SSH (port 22). Off by default — the mesh
|
||||||
|
firewall refuses SSH until you allow it here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="path-action-button path-action-button--continue px-4 py-2 text-sm shrink-0"
|
||||||
|
:disabled="saving || loading"
|
||||||
|
:class="{ '!bg-orange-500/30 !border-orange-400/50': status?.enabled }"
|
||||||
|
@click="toggle(!status?.enabled)"
|
||||||
|
>
|
||||||
|
{{ status?.enabled ? 'Allowed — block again' : 'Allow SSH over mesh' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="text-sm text-white/50 py-3">Loading…</div>
|
||||||
|
<div v-else-if="error" class="text-sm text-red-300 py-3">{{ error }}
|
||||||
|
<button class="ml-2 underline" @click="refresh">Retry</button>
|
||||||
|
</div>
|
||||||
|
<template v-else-if="status">
|
||||||
|
<!-- Preflights: explain the toggle rather than gating it -->
|
||||||
|
<div class="mt-3 space-y-1.5 text-sm">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span :class="status.preflights?.sshd_active ? 'text-green-400' : 'text-orange-300'">●</span>
|
||||||
|
<span class="text-white/80">sshd {{ status.preflights?.sshd_active ? 'is running' : 'is NOT running' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span :class="status.preflights?.sshd_ipv6_listen ? 'text-green-400' : 'text-orange-300'">●</span>
|
||||||
|
<span class="text-white/80">
|
||||||
|
{{ status.preflights?.sshd_ipv6_listen ? 'listens on IPv6 (mesh-reachable)' : 'does not listen on IPv6 — the mesh cannot reach it' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span :class="status.preflights?.password_auth === false ? 'text-green-400' : 'text-orange-300'">●</span>
|
||||||
|
<span class="text-white/80">
|
||||||
|
{{ status.preflights?.password_auth == null
|
||||||
|
? 'PasswordAuthentication unknown'
|
||||||
|
: status.preflights?.password_auth
|
||||||
|
? 'password login allowed — keys-only is the safer pairing for the firewall rule'
|
||||||
|
: 'keys-only login (recommended)' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Source restriction -->
|
||||||
|
<div class="mt-4">
|
||||||
|
<p class="text-xs text-white/50 mb-1">Restrict to specific mesh addresses (one per line), or leave empty to allow any mesh peer.</p>
|
||||||
|
<textarea
|
||||||
|
v-model="sourcesText"
|
||||||
|
rows="2"
|
||||||
|
class="w-full bg-white/10 border border-white/20 rounded-lg p-2 text-sm text-white/90 font-mono placeholder-white/30"
|
||||||
|
placeholder="fd79:… phone mesh address"
|
||||||
|
:disabled="!status.enabled || saving"
|
||||||
|
@change="apply(true, sourcesText.split(/[\n,]+/).map(s => s.trim()).filter(Boolean))"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Phone-side hint: the npub is the durable address -->
|
||||||
|
<div v-if="status.enabled && fipsNpub" class="mt-3 flex items-center gap-2 text-sm">
|
||||||
|
<span class="text-white/60">From Termux:</span>
|
||||||
|
<code class="text-orange-200 truncate max-w-[55%]">fipssh <user>@{{ fipsNpub.slice(0, 12) }}…</code>
|
||||||
|
<button class="glass-button px-2 py-1 text-xs" @click="copyFipssh">Copy</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Danger-zone confirmation for the unrestricted scope -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div v-if="confirmAnyPeer" class="fixed inset-0 z-[3000] flex items-center justify-center p-4" @click.self="confirmAnyPeer = false">
|
||||||
|
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||||
|
<div class="glass-card p-6 max-w-md w-full relative z-10">
|
||||||
|
<h3 class="text-lg font-semibold text-white mb-3">Allow SSH from ANY mesh peer?</h3>
|
||||||
|
<p class="text-sm text-white/70 mb-4">
|
||||||
|
Anyone who can route to this node over the FIPS mesh will reach port 22. Restricting
|
||||||
|
to your phone's mesh address above is the safer pairing. Only continue if you
|
||||||
|
understand port 22 will face every mesh peer.
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button class="flex-1 glass-button px-4 py-2 text-sm" @click="confirmAnyPeer = false">Restrict instead</button>
|
||||||
|
<button
|
||||||
|
class="flex-1 px-4 py-2 rounded-lg text-sm border border-orange-400/50 bg-orange-500/20 text-orange-200 hover:bg-orange-500/30"
|
||||||
|
:disabled="saving || pendingEnableAny === false"
|
||||||
|
@click="apply(true, [])"
|
||||||
|
>I understand — allow any</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -6,6 +6,7 @@ import AIDataAccessSection from '@/views/settings/AIDataAccessSection.vue'
|
|||||||
import RoutstrBudgetSection from '@/views/settings/RoutstrBudgetSection.vue'
|
import RoutstrBudgetSection from '@/views/settings/RoutstrBudgetSection.vue'
|
||||||
import WebhookSection from '@/views/settings/WebhookSection.vue'
|
import WebhookSection from '@/views/settings/WebhookSection.vue'
|
||||||
import TelemetrySection from '@/views/settings/TelemetrySection.vue'
|
import TelemetrySection from '@/views/settings/TelemetrySection.vue'
|
||||||
|
import SshOverMeshCard from '@/views/settings/SshOverMeshCard.vue'
|
||||||
import NodeCertificateSection from '@/views/settings/NodeCertificateSection.vue'
|
import NodeCertificateSection from '@/views/settings/NodeCertificateSection.vue'
|
||||||
import LightningCredentialsSection from '@/views/settings/LightningCredentialsSection.vue'
|
import LightningCredentialsSection from '@/views/settings/LightningCredentialsSection.vue'
|
||||||
import BackupSection from '@/views/settings/BackupSection.vue'
|
import BackupSection from '@/views/settings/BackupSection.vue'
|
||||||
@@ -20,6 +21,7 @@ import SystemDangerZone from '@/views/settings/SystemDangerZone.vue'
|
|||||||
<AIDataAccessSection />
|
<AIDataAccessSection />
|
||||||
<WebhookSection />
|
<WebhookSection />
|
||||||
<TelemetrySection />
|
<TelemetrySection />
|
||||||
|
<SshOverMeshCard />
|
||||||
<NodeCertificateSection />
|
<NodeCertificateSection />
|
||||||
<LightningCredentialsSection />
|
<LightningCredentialsSection />
|
||||||
<BackupSection />
|
<BackupSection />
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach } from 'vitest'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { createPinia, setActivePinia } from 'pinia'
|
||||||
|
|
||||||
|
const getSpy = vi.fn()
|
||||||
|
const setSpy = vi.fn()
|
||||||
|
const toast = { success: vi.fn(), error: vi.fn(), info: vi.fn() }
|
||||||
|
|
||||||
|
vi.mock('@/api/rpc-client', () => ({
|
||||||
|
rpcClient: {
|
||||||
|
call: (req: { method: string; params?: unknown }) => {
|
||||||
|
if (req.method === 'fips.ssh-over-mesh.get') return getSpy(req)
|
||||||
|
if (req.method === 'fips.ssh-over-mesh.set') return setSpy(req)
|
||||||
|
if (req.method === 'fips.status') return Promise.resolve({ npub: 'npub1test' })
|
||||||
|
return Promise.resolve({})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
vi.mock('@/composables/useToast', () => ({ useToast: () => toast }))
|
||||||
|
|
||||||
|
import SshOverMeshCard from '../SshOverMeshCard.vue'
|
||||||
|
|
||||||
|
function statusBody(overrides: Record<string, unknown> = {}) {
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
sources: [],
|
||||||
|
scope: 'any',
|
||||||
|
preflights: { sshd_active: true, sshd_ipv6_listen: true, password_auth: false },
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('SshOverMeshCard', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia())
|
||||||
|
vi.clearAllMocks()
|
||||||
|
getSpy.mockResolvedValue(statusBody())
|
||||||
|
setSpy.mockResolvedValue(statusBody({ enabled: true }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders preflights from the RPC state', async () => {
|
||||||
|
getSpy.mockResolvedValue(statusBody())
|
||||||
|
const wrapper = mount(SshOverMeshCard)
|
||||||
|
await vi.dynamicImportSettled()
|
||||||
|
await new Promise(r => setTimeout(r, 0))
|
||||||
|
expect(getSpy).toHaveBeenCalledWith(expect.objectContaining({ method: 'fips.ssh-over-mesh.get' }))
|
||||||
|
expect(wrapper.text()).toContain('sshd is running')
|
||||||
|
expect(wrapper.text()).toContain('keys-only login')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('flags the missing IPv6 listener honestly', async () => {
|
||||||
|
getSpy.mockResolvedValue(statusBody({
|
||||||
|
preflights: { sshd_active: true, sshd_ipv6_listen: false, password_auth: true },
|
||||||
|
}))
|
||||||
|
const wrapper = mount(SshOverMeshCard)
|
||||||
|
await new Promise(r => setTimeout(r, 0))
|
||||||
|
expect(wrapper.text()).toContain('does not listen on IPv6')
|
||||||
|
expect(wrapper.text()).toContain('keys-only is the safer pairing')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('demands the danger-zone confirmation before an unrestricted enable', async () => {
|
||||||
|
const wrapper = mount(SshOverMeshCard)
|
||||||
|
await new Promise(r => setTimeout(r, 0))
|
||||||
|
await wrapper.find('button.path-action-button').trigger('click')
|
||||||
|
// The confirmation modal Teleports to <body>; nothing applied yet.
|
||||||
|
expect(document.body.textContent).toContain('Allow SSH from ANY mesh peer')
|
||||||
|
expect(setSpy).not.toHaveBeenCalled()
|
||||||
|
// Confirming applies the unrestricted rule.
|
||||||
|
const confirm = [...document.body.querySelectorAll('button')]
|
||||||
|
.find(b => (b.textContent ?? '').includes('I understand'))
|
||||||
|
expect(confirm).toBeDefined()
|
||||||
|
confirm!.dispatchEvent(new Event('click'))
|
||||||
|
await new Promise(r => setTimeout(r, 0))
|
||||||
|
await new Promise(r => setTimeout(r, 0))
|
||||||
|
expect(setSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
params: { enabled: true, sources: [] },
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('applies a restricted enable without the any-peer confirmation', async () => {
|
||||||
|
// An existing restricted rule: turning it back on re-applies the list.
|
||||||
|
getSpy.mockResolvedValue(statusBody({ enabled: false, sources: ['fd00::1'], scope: 'list' }))
|
||||||
|
setSpy.mockResolvedValue(statusBody({ enabled: true, sources: ['fd00::1'], scope: 'list' }))
|
||||||
|
const wrapper = mount(SshOverMeshCard)
|
||||||
|
await new Promise(r => setTimeout(r, 0))
|
||||||
|
await wrapper.find('button.path-action-button').trigger('click')
|
||||||
|
expect(setSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
params: { enabled: true, sources: ['fd00::1'] },
|
||||||
|
}))
|
||||||
|
expect(wrapper.text()).not.toContain('Allow SSH from ANY mesh peer')
|
||||||
|
})
|
||||||
|
})
|
||||||
+19
-16
@@ -1,29 +1,32 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"**Companion 0.5.28 is included in the node download this time, with the work that missed v1.8.5.** The companion hub can back up and restore its node list, act as a NIP-46 remote signer, and shows each paired node's FIPS mesh address with tap-to-copy. For Termux users, the included `fipssh` helper turns a durable node npub into its mesh address, so `fipssh user@npub1…` can reach SSH once that node has explicitly allowed port 22. The node-side “SSH over mesh” firewall toggle is not claimed here—it still needs implementation and remains off by default.",
|
"**What's New really does stop at v1.8.0 now.** The first correction removed old generated release blocks but missed six much older hand-written v1.2 sections at the bottom of the modal. Those sections are gone, and the release check now recognizes and rejects that legacy format too, so the history floor cannot falsely pass again.",
|
||||||
"**What's New now starts cleanly at v1.8.0 and is guaranteed to be newest-first.** Older alpha history no longer overwhelms the useful recent changes, the three stray v1.7 entries that appeared above current releases are gone, and the release check now fails if either the ordering or the v1.8.0 history floor drifts again.",
|
"**The installer carries the same corrected release and Companion 0.5.28.** Its artifact gate now checks the companion APK version and the v1.8.0 What's New floor inside the finished ISO, so a stale frontend or phone app cannot be published under the current release label.",
|
||||||
"**A release can no longer advertise itself before its files exist.** New releases are prepared behind a pending manifest; the publisher uploads the backend and frontend, downloads both back and verifies their size and hash, and only then promotes the signed manifest to the path nodes read. The manifest generator also includes every curated What's New item instead of silently stopping after the first ten physical changelog lines."
|
"**Crash dumps work on fresh installs as well as upgraded nodes.** The installer gate checks every kdump package inside the finished ISO, and `makedumpfile` is installed explicitly rather than accidentally relying on a recommended dependency that the minimal image deliberately omits.",
|
||||||
|
"**Apps open over HTTPS when your node does.** Connect to your node over HTTPS and the apps you open — Vaultwarden in its own tab, BTCPay, Grafana, and the rest, on a remote browser or in the phone's in-app browser — now open on the same secure connection instead of silently dropping to plain HTTP. The node's app gate already served TLS on every app port; the dashboard was handing out `http://` addresses regardless of how you reached it. Ports the gate does not front (plain-HTTP publishes, and the API ports like Cuprate's RPC) deliberately stay on `http` — `https` there would simply fail to connect. Plain-HTTP access (the kiosk, LAN browsing) is unchanged.",
|
||||||
|
"**Every app in the store is now a first-class platform app.** The last stragglers — Nginx Proxy Manager, Tailscale, Ollama, CryptPad, and AdGuard Home — now carry full manifests: the node's app gate fronts their web ports (TLS on the same port, the node login where appropriate, embedding fixes, Tor), installs go through the orchestrator like every other app, and their pins live in the signed catalog. Ollama stays loopback-only — it is the assistant's local model backend, not a web app. The four apps retired earlier (FIPS, Nostr VPN, Routstr, Penpot) are finally dropped from the catalog, and Cuprate's manifest — which carried a duplicated metadata block that strict parsers reject — is fixed.",
|
||||||
|
"**Newly signed apps appear in the App Store immediately.** The App Store now serves the release-signed catalog the node has already fetched and verified — so publishing a signed app (like Cuprate) makes it appear for every updated node without waiting for a dashboard release. The unsigned community catalog remains only as a fallback for nodes that can't reach the registry. The same signed catalog now also decides which ports serve TLS, so nothing is upgraded to `https` that can't answer it."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"current_version": "1.8.6-alpha",
|
"current_version": "1.8.7-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago",
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"new_version": "1.8.6-alpha",
|
"new_version": "1.8.7-alpha",
|
||||||
"sha256": "6171e1f46b0e2c2754a18e668f16810338a09b71a68dc365d65173ca1e4d400a",
|
"sha256": "572accec81e73fbcd5218ddc41f5ec719deda30f104c838398fdfa85a7a276f0",
|
||||||
"size_bytes": 63930112
|
"size_bytes": 63990960
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current_version": "1.8.6-alpha",
|
"current_version": "1.8.7-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago-frontend-1.8.6-alpha.tar.gz",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago-frontend-1.8.7-alpha.tar.gz",
|
||||||
"name": "archipelago-frontend-1.8.6-alpha.tar.gz",
|
"name": "archipelago-frontend-1.8.7-alpha.tar.gz",
|
||||||
"new_version": "1.8.6-alpha",
|
"new_version": "1.8.7-alpha",
|
||||||
"sha256": "1a5b54c516f91a4ea4491e08182f0f9521ee7e27935388814007f91695f06e0f",
|
"sha256": "26b8c12b1b3e6b5c93841f3713037f65d0ea6a52401bd28888eb8d2f5dc483b5",
|
||||||
"size_bytes": 97769923
|
"size_bytes": 97779181
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release_date": "2026-08-31",
|
"release_date": "2026-08-31",
|
||||||
"signature": "c21dc5cb5a6caa3253c08835b65d0417a312bc8c51acdf92df990ad2110ae762b77287c479a523a8521827ec7b4695114ff9c39924cf1d30d0d622abb53b9208",
|
"signature": "79332435d436e7bae1a5b0c12158b02083b681b6818ac90219021f1e39eb61a3e1039ed38ee8089006a0dec862fd92997e44a6c9ccf0be17e91a95cba5de6402",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"version": "1.8.6-alpha"
|
"version": "1.8.7-alpha"
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-329
@@ -17,7 +17,7 @@
|
|||||||
"description": "Network-wide ad and tracker blocking: a DNS server that filters every device on your LAN, with a web console for rules and client management.",
|
"description": "Network-wide ad and tracker blocking: a DNS server that filters every device on your LAN, with a web console for rules and client management.",
|
||||||
"environment": [],
|
"environment": [],
|
||||||
"health_check": {
|
"health_check": {
|
||||||
"endpoint": "localhost:3000",
|
"endpoint": "localhost:3030",
|
||||||
"interval": "30s",
|
"interval": "30s",
|
||||||
"retries": 3,
|
"retries": 3,
|
||||||
"timeout": "5s",
|
"timeout": "5s",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"description": "AdGuard Home web console",
|
"description": "AdGuard Home web console",
|
||||||
"name": "Admin console",
|
"name": "Admin console",
|
||||||
"path": "/",
|
"path": "/",
|
||||||
"port": 3000,
|
"port": 3030,
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"type": "ui"
|
"type": "ui"
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
"auth_rationale": "AdGuard Home enforces its own admin login on the console, and the first-run wizard must answer before any account exists.",
|
"auth_rationale": "AdGuard Home enforces its own admin login on the console, and the first-run wizard must answer before any account exists.",
|
||||||
"bind": "127.0.0.1",
|
"bind": "127.0.0.1",
|
||||||
"container": 3000,
|
"container": 3000,
|
||||||
"host": 3000,
|
"host": 3030,
|
||||||
"protocol": "tcp"
|
"protocol": "tcp"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1297,86 +1297,6 @@
|
|||||||
},
|
},
|
||||||
"version": "23.08.2"
|
"version": "23.08.2"
|
||||||
},
|
},
|
||||||
"cryptpad": {
|
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0",
|
|
||||||
"manifest": {
|
|
||||||
"app": {
|
|
||||||
"container": {
|
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/cryptpad:2024.12.0",
|
|
||||||
"network": "pasta",
|
|
||||||
"pull_policy": "if-not-present"
|
|
||||||
},
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"storage": "5Gi"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "End-to-end encrypted documents, spreadsheets, and presentations. Zero-knowledge collaboration.",
|
|
||||||
"environment": [],
|
|
||||||
"health_check": {
|
|
||||||
"endpoint": "localhost:3000",
|
|
||||||
"interval": "30s",
|
|
||||||
"retries": 3,
|
|
||||||
"timeout": "5s",
|
|
||||||
"type": "tcp"
|
|
||||||
},
|
|
||||||
"id": "cryptpad",
|
|
||||||
"interfaces": {
|
|
||||||
"main": {
|
|
||||||
"description": "Encrypted collaboration suite",
|
|
||||||
"name": "CryptPad",
|
|
||||||
"path": "/",
|
|
||||||
"port": 3000,
|
|
||||||
"protocol": "http",
|
|
||||||
"type": "ui"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"author": "XWiki SAS",
|
|
||||||
"category": "data",
|
|
||||||
"icon": "/assets/icon/favico-black-v2.svg",
|
|
||||||
"repo": "https://github.com/cryptpad/cryptpad",
|
|
||||||
"tier": "optional"
|
|
||||||
},
|
|
||||||
"name": "CryptPad",
|
|
||||||
"ports": [
|
|
||||||
{
|
|
||||||
"auth": "gated",
|
|
||||||
"bind": "127.0.0.1",
|
|
||||||
"container": 3000,
|
|
||||||
"host": 3000,
|
|
||||||
"protocol": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"resources": {
|
|
||||||
"disk_limit": "5Gi",
|
|
||||||
"memory_limit": "1Gi"
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"capabilities": [],
|
|
||||||
"network_policy": "isolated",
|
|
||||||
"no_new_privileges": true,
|
|
||||||
"readonly_root": false
|
|
||||||
},
|
|
||||||
"upstream": {
|
|
||||||
"kind": "github",
|
|
||||||
"repo": "cryptpad/cryptpad"
|
|
||||||
},
|
|
||||||
"version": "2024.12.0",
|
|
||||||
"volumes": [
|
|
||||||
{
|
|
||||||
"options": [
|
|
||||||
"rw"
|
|
||||||
],
|
|
||||||
"source": "/var/lib/archipelago/cryptpad",
|
|
||||||
"target": "/cryptpad/data",
|
|
||||||
"type": "bind"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"version": "2024.12.0"
|
|
||||||
},
|
|
||||||
"cuprate": {
|
"cuprate": {
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
@@ -1467,79 +1387,6 @@
|
|||||||
},
|
},
|
||||||
"version": "0.1.0-preview"
|
"version": "0.1.0-preview"
|
||||||
},
|
},
|
||||||
"did-wallet": {
|
|
||||||
"manifest": {
|
|
||||||
"app": {
|
|
||||||
"container": {
|
|
||||||
"image": "archipelago/did-wallet:1.0.0",
|
|
||||||
"image_signature": "cosign://...",
|
|
||||||
"pull_policy": "if-not-present"
|
|
||||||
},
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"storage": "2Gi"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Web5 wallet with Decentralized Identifier (DID) support. Manage your digital identity and Web5 assets.",
|
|
||||||
"environment": [
|
|
||||||
"WALLET_STORAGE=/app/wallet"
|
|
||||||
],
|
|
||||||
"health_check": {
|
|
||||||
"endpoint": "http://127.0.0.1:8080",
|
|
||||||
"interval": "30s",
|
|
||||||
"path": "/health",
|
|
||||||
"retries": 3,
|
|
||||||
"timeout": "5s",
|
|
||||||
"type": "http"
|
|
||||||
},
|
|
||||||
"id": "did-wallet",
|
|
||||||
"name": "Web5 DID Wallet",
|
|
||||||
"ports": [
|
|
||||||
{
|
|
||||||
"auth": "gated",
|
|
||||||
"bind": "127.0.0.1",
|
|
||||||
"container": 8080,
|
|
||||||
"host": 8088,
|
|
||||||
"protocol": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"resources": {
|
|
||||||
"cpu_limit": 1,
|
|
||||||
"disk_limit": "2Gi",
|
|
||||||
"memory_limit": "512Mi"
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"apparmor_profile": "did-wallet",
|
|
||||||
"capabilities": [],
|
|
||||||
"network_policy": "isolated",
|
|
||||||
"no_new_privileges": true,
|
|
||||||
"readonly_root": true,
|
|
||||||
"seccomp_profile": "default",
|
|
||||||
"user": 1000
|
|
||||||
},
|
|
||||||
"upstream": {
|
|
||||||
"kind": "internal"
|
|
||||||
},
|
|
||||||
"version": "1.0.0",
|
|
||||||
"volumes": [
|
|
||||||
{
|
|
||||||
"options": [
|
|
||||||
"rw"
|
|
||||||
],
|
|
||||||
"source": "/var/lib/archipelago/did-wallet",
|
|
||||||
"target": "/app/wallet",
|
|
||||||
"type": "bind"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"web5_integration": {
|
|
||||||
"bitcoin_integration": true,
|
|
||||||
"did_support": true,
|
|
||||||
"wallet_functionality": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"electrs-ui": {
|
"electrs-ui": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/electrs-ui:1.7.123-alpha",
|
"image": "source.archipelago-foundation.org/lfg2025/electrs-ui:1.7.123-alpha",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
@@ -3360,106 +3207,6 @@
|
|||||||
},
|
},
|
||||||
"version": "10.11.11"
|
"version": "10.11.11"
|
||||||
},
|
},
|
||||||
"lightning-stack": {
|
|
||||||
"manifest": {
|
|
||||||
"app": {
|
|
||||||
"bitcoin_integration": {
|
|
||||||
"rpc_access": "admin",
|
|
||||||
"sync_required": true
|
|
||||||
},
|
|
||||||
"container": {
|
|
||||||
"image": "lightninglabs/lightning-stack:v0.12.0",
|
|
||||||
"image_signature": "cosign://...",
|
|
||||||
"pull_policy": "if-not-present"
|
|
||||||
},
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"app_id": "bitcoin-core",
|
|
||||||
"version": ">=24.0"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"storage": "50Gi"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "Complete Lightning Network implementation. Includes LND, CLN, and management tools.",
|
|
||||||
"environment": [
|
|
||||||
"BITCOIND_HOST=bitcoin-core",
|
|
||||||
"BITCOIND_RPCUSER=${BITCOIN_RPC_USER}",
|
|
||||||
"BITCOIND_RPCPASS=${BITCOIN_RPC_PASSWORD}",
|
|
||||||
"NETWORK=mainnet"
|
|
||||||
],
|
|
||||||
"health_check": {
|
|
||||||
"endpoint": "http://127.0.0.1:8080",
|
|
||||||
"interval": "30s",
|
|
||||||
"path": "/v1/getinfo",
|
|
||||||
"retries": 3,
|
|
||||||
"timeout": "5s",
|
|
||||||
"type": "http"
|
|
||||||
},
|
|
||||||
"id": "lightning-stack",
|
|
||||||
"lightning_integration": {
|
|
||||||
"channel_management": true,
|
|
||||||
"payment_routing": true
|
|
||||||
},
|
|
||||||
"name": "Lightning Stack",
|
|
||||||
"ports": [
|
|
||||||
{
|
|
||||||
"auth": "none",
|
|
||||||
"auth_rationale": "Lightning p2p. The BOLT-8 noise handshake authenticates and encrypts the channel itself.",
|
|
||||||
"container": 9735,
|
|
||||||
"host": 9738,
|
|
||||||
"protocol": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"auth": "none",
|
|
||||||
"auth_rationale": "LND gRPC, authenticated by macaroon over TLS. Remote wallets depend on reaching this directly.",
|
|
||||||
"container": 10009,
|
|
||||||
"host": 10010,
|
|
||||||
"protocol": "tcp"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"auth": "none",
|
|
||||||
"auth_rationale": "LND REST, authenticated by macaroon over TLS. A browser login page would break Zeus and every non-browser wallet client, exactly as for lnd's 18080.",
|
|
||||||
"container": 8080,
|
|
||||||
"host": 8091,
|
|
||||||
"protocol": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"resources": {
|
|
||||||
"cpu_limit": 4,
|
|
||||||
"disk_limit": "50Gi",
|
|
||||||
"memory_limit": "4Gi"
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"apparmor_profile": "lightning-stack",
|
|
||||||
"capabilities": [
|
|
||||||
"NET_BIND_SERVICE"
|
|
||||||
],
|
|
||||||
"network_policy": "isolated",
|
|
||||||
"no_new_privileges": true,
|
|
||||||
"readonly_root": true,
|
|
||||||
"seccomp_profile": "default",
|
|
||||||
"user": 1000
|
|
||||||
},
|
|
||||||
"upstream": {
|
|
||||||
"kind": "manual",
|
|
||||||
"url": "no public listing for lightninglabs/lightning-stack — verify by hand"
|
|
||||||
},
|
|
||||||
"version": "0.12.0",
|
|
||||||
"volumes": [
|
|
||||||
{
|
|
||||||
"options": [
|
|
||||||
"rw"
|
|
||||||
],
|
|
||||||
"source": "/var/lib/archipelago/lightning-stack",
|
|
||||||
"target": "/root/.lightning",
|
|
||||||
"type": "bind"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"version": "0.12.0"
|
|
||||||
},
|
|
||||||
"lnd": {
|
"lnd": {
|
||||||
"image": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
|
"image": "source.archipelago-foundation.org/lfg2025/lnd:v0.18.4-beta",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
@@ -3815,75 +3562,6 @@
|
|||||||
},
|
},
|
||||||
"version": "3.0.0"
|
"version": "3.0.0"
|
||||||
},
|
},
|
||||||
"morphos-server": {
|
|
||||||
"manifest": {
|
|
||||||
"app": {
|
|
||||||
"container": {
|
|
||||||
"image": "archipelago/morphos-server:1.0.0",
|
|
||||||
"image_signature": "cosign://...",
|
|
||||||
"pull_policy": "if-not-present"
|
|
||||||
},
|
|
||||||
"dependencies": [
|
|
||||||
{
|
|
||||||
"storage": "5Gi"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"description": "MorphOS server platform. Decentralized application server.",
|
|
||||||
"environment": [
|
|
||||||
"MORPHOS_ENV=production",
|
|
||||||
"MORPHOS_DATA_DIR=/app/data"
|
|
||||||
],
|
|
||||||
"health_check": {
|
|
||||||
"endpoint": "http://127.0.0.1:8080",
|
|
||||||
"interval": "30s",
|
|
||||||
"path": "/health",
|
|
||||||
"retries": 3,
|
|
||||||
"timeout": "5s",
|
|
||||||
"type": "http"
|
|
||||||
},
|
|
||||||
"id": "morphos-server",
|
|
||||||
"name": "MorphOS Server",
|
|
||||||
"ports": [
|
|
||||||
{
|
|
||||||
"auth": "gated",
|
|
||||||
"bind": "127.0.0.1",
|
|
||||||
"container": 8080,
|
|
||||||
"host": 8089,
|
|
||||||
"protocol": "tcp"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"resources": {
|
|
||||||
"cpu_limit": 2,
|
|
||||||
"disk_limit": "5Gi",
|
|
||||||
"memory_limit": "2Gi"
|
|
||||||
},
|
|
||||||
"security": {
|
|
||||||
"apparmor_profile": "morphos-server",
|
|
||||||
"capabilities": [],
|
|
||||||
"network_policy": "isolated",
|
|
||||||
"no_new_privileges": true,
|
|
||||||
"readonly_root": true,
|
|
||||||
"seccomp_profile": "default",
|
|
||||||
"user": 1000
|
|
||||||
},
|
|
||||||
"upstream": {
|
|
||||||
"kind": "internal"
|
|
||||||
},
|
|
||||||
"version": "1.0.0",
|
|
||||||
"volumes": [
|
|
||||||
{
|
|
||||||
"options": [
|
|
||||||
"rw"
|
|
||||||
],
|
|
||||||
"source": "/var/lib/archipelago/morphos-server",
|
|
||||||
"target": "/app/data",
|
|
||||||
"type": "bind"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"version": "1.0.0"
|
|
||||||
},
|
|
||||||
"netbird": {
|
"netbird": {
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"app": {
|
"app": {
|
||||||
@@ -4514,8 +4192,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"resources": {
|
"resources": {
|
||||||
"disk_limit": "50Gi",
|
"disk_limit": "50Gi"
|
||||||
"memory_limit": 0
|
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
"capabilities": [],
|
"capabilities": [],
|
||||||
@@ -5743,7 +5420,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"schema": 1,
|
"schema": 1,
|
||||||
"signature": "3b30d9e1afd59d2de4e9e48e5556c8c4ff54d13ae1f25ac2b5897aead6aa8ae3a503dfe56232329e8b02226ea98d3415ff220b4fcdf40913cfe9300aa99fc807",
|
"signature": "03941cb0dd7ec0ce344e4231da40606f1ef24d34a7f1f872622c42bd982846ec622e5161839a68824be4d386fcc68c4958624daf5249897c620a835dc2d11304",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"updated": "2026-08-31"
|
"updated": "2026-09-01"
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-16
@@ -1,29 +1,32 @@
|
|||||||
{
|
{
|
||||||
"changelog": [
|
"changelog": [
|
||||||
"**Companion 0.5.28 is included in the node download this time, with the work that missed v1.8.5.** The companion hub can back up and restore its node list, act as a NIP-46 remote signer, and shows each paired node's FIPS mesh address with tap-to-copy. For Termux users, the included `fipssh` helper turns a durable node npub into its mesh address, so `fipssh user@npub1…` can reach SSH once that node has explicitly allowed port 22. The node-side “SSH over mesh” firewall toggle is not claimed here—it still needs implementation and remains off by default.",
|
"**What's New really does stop at v1.8.0 now.** The first correction removed old generated release blocks but missed six much older hand-written v1.2 sections at the bottom of the modal. Those sections are gone, and the release check now recognizes and rejects that legacy format too, so the history floor cannot falsely pass again.",
|
||||||
"**What's New now starts cleanly at v1.8.0 and is guaranteed to be newest-first.** Older alpha history no longer overwhelms the useful recent changes, the three stray v1.7 entries that appeared above current releases are gone, and the release check now fails if either the ordering or the v1.8.0 history floor drifts again.",
|
"**The installer carries the same corrected release and Companion 0.5.28.** Its artifact gate now checks the companion APK version and the v1.8.0 What's New floor inside the finished ISO, so a stale frontend or phone app cannot be published under the current release label.",
|
||||||
"**A release can no longer advertise itself before its files exist.** New releases are prepared behind a pending manifest; the publisher uploads the backend and frontend, downloads both back and verifies their size and hash, and only then promotes the signed manifest to the path nodes read. The manifest generator also includes every curated What's New item instead of silently stopping after the first ten physical changelog lines."
|
"**Crash dumps work on fresh installs as well as upgraded nodes.** The installer gate checks every kdump package inside the finished ISO, and `makedumpfile` is installed explicitly rather than accidentally relying on a recommended dependency that the minimal image deliberately omits.",
|
||||||
|
"**Apps open over HTTPS when your node does.** Connect to your node over HTTPS and the apps you open — Vaultwarden in its own tab, BTCPay, Grafana, and the rest, on a remote browser or in the phone's in-app browser — now open on the same secure connection instead of silently dropping to plain HTTP. The node's app gate already served TLS on every app port; the dashboard was handing out `http://` addresses regardless of how you reached it. Ports the gate does not front (plain-HTTP publishes, and the API ports like Cuprate's RPC) deliberately stay on `http` — `https` there would simply fail to connect. Plain-HTTP access (the kiosk, LAN browsing) is unchanged.",
|
||||||
|
"**Every app in the store is now a first-class platform app.** The last stragglers — Nginx Proxy Manager, Tailscale, Ollama, CryptPad, and AdGuard Home — now carry full manifests: the node's app gate fronts their web ports (TLS on the same port, the node login where appropriate, embedding fixes, Tor), installs go through the orchestrator like every other app, and their pins live in the signed catalog. Ollama stays loopback-only — it is the assistant's local model backend, not a web app. The four apps retired earlier (FIPS, Nostr VPN, Routstr, Penpot) are finally dropped from the catalog, and Cuprate's manifest — which carried a duplicated metadata block that strict parsers reject — is fixed.",
|
||||||
|
"**Newly signed apps appear in the App Store immediately.** The App Store now serves the release-signed catalog the node has already fetched and verified — so publishing a signed app (like Cuprate) makes it appear for every updated node without waiting for a dashboard release. The unsigned community catalog remains only as a fallback for nodes that can't reach the registry. The same signed catalog now also decides which ports serve TLS, so nothing is upgraded to `https` that can't answer it."
|
||||||
],
|
],
|
||||||
"components": [
|
"components": [
|
||||||
{
|
{
|
||||||
"current_version": "1.8.6-alpha",
|
"current_version": "1.8.7-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago",
|
||||||
"name": "archipelago",
|
"name": "archipelago",
|
||||||
"new_version": "1.8.6-alpha",
|
"new_version": "1.8.7-alpha",
|
||||||
"sha256": "6171e1f46b0e2c2754a18e668f16810338a09b71a68dc365d65173ca1e4d400a",
|
"sha256": "572accec81e73fbcd5218ddc41f5ec719deda30f104c838398fdfa85a7a276f0",
|
||||||
"size_bytes": 63930112
|
"size_bytes": 63990960
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"current_version": "1.8.6-alpha",
|
"current_version": "1.8.7-alpha",
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.6-alpha/archipelago-frontend-1.8.6-alpha.tar.gz",
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago-frontend-1.8.7-alpha.tar.gz",
|
||||||
"name": "archipelago-frontend-1.8.6-alpha.tar.gz",
|
"name": "archipelago-frontend-1.8.7-alpha.tar.gz",
|
||||||
"new_version": "1.8.6-alpha",
|
"new_version": "1.8.7-alpha",
|
||||||
"sha256": "1a5b54c516f91a4ea4491e08182f0f9521ee7e27935388814007f91695f06e0f",
|
"sha256": "26b8c12b1b3e6b5c93841f3713037f65d0ea6a52401bd28888eb8d2f5dc483b5",
|
||||||
"size_bytes": 97769923
|
"size_bytes": 97779181
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"release_date": "2026-08-31",
|
"release_date": "2026-08-31",
|
||||||
"signature": "c21dc5cb5a6caa3253c08835b65d0417a312bc8c51acdf92df990ad2110ae762b77287c479a523a8521827ec7b4695114ff9c39924cf1d30d0d622abb53b9208",
|
"signature": "79332435d436e7bae1a5b0c12158b02083b681b6818ac90219021f1e39eb61a3e1039ed38ee8089006a0dec862fd92997e44a6c9ccf0be17e91a95cba5de6402",
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
"version": "1.8.6-alpha"
|
"version": "1.8.7-alpha"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"changelog": [
|
|
||||||
"**What's New really does stop at v1.8.0 now.** The first correction removed old generated release blocks but missed six much older hand-written v1.2 sections at the bottom of the modal. Those sections are gone, and the release check now recognizes and rejects that legacy format too, so the history floor cannot falsely pass again.",
|
|
||||||
"**The installer carries the same corrected release and Companion 0.5.28.** Its artifact gate now checks the companion APK version and the v1.8.0 What's New floor inside the finished ISO, so a stale frontend or phone app cannot be published under the current release label.",
|
|
||||||
"**Crash dumps work on fresh installs as well as upgraded nodes.** The installer gate checks every kdump package inside the finished ISO, and `makedumpfile` is installed explicitly rather than accidentally relying on a recommended dependency that the minimal image deliberately omits.",
|
|
||||||
"**Apps open over HTTPS when your node does.** Connect to your node over HTTPS and the apps you open — Vaultwarden in its own tab, BTCPay, Grafana, and the rest, on a remote browser or in the phone's in-app browser — now open on the same secure connection instead of silently dropping to plain HTTP. The node's app gate already served TLS on every app port; the dashboard was handing out `http://` addresses regardless of how you reached it. Ports the gate does not front (plain-HTTP publishes, and the API ports like Cuprate's RPC) deliberately stay on `http` — `https` there would simply fail to connect. Plain-HTTP access (the kiosk, LAN browsing) is unchanged.",
|
|
||||||
"**Every app in the store is now a first-class platform app.** The last stragglers — Nginx Proxy Manager, Tailscale, Ollama, CryptPad, and AdGuard Home — now carry full manifests: the node's app gate fronts their web ports (TLS on the same port, the node login where appropriate, embedding fixes, Tor), installs go through the orchestrator like every other app, and their pins live in the signed catalog. Ollama stays loopback-only — it is the assistant's local model backend, not a web app. The four apps retired earlier (FIPS, Nostr VPN, Routstr, Penpot) are finally dropped from the catalog, and Cuprate's manifest — which carried a duplicated metadata block that strict parsers reject — is fixed.",
|
|
||||||
"**Newly signed apps appear in the App Store immediately.** The App Store now serves the release-signed catalog the node has already fetched and verified — so publishing a signed app (like Cuprate) makes it appear for every updated node without waiting for a dashboard release. The unsigned community catalog remains only as a fallback for nodes that can't reach the registry. The same signed catalog now also decides which ports serve TLS, so nothing is upgraded to `https` that can't answer it."
|
|
||||||
],
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"current_version": "1.8.7-alpha",
|
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago",
|
|
||||||
"name": "archipelago",
|
|
||||||
"new_version": "1.8.7-alpha",
|
|
||||||
"sha256": "572accec81e73fbcd5218ddc41f5ec719deda30f104c838398fdfa85a7a276f0",
|
|
||||||
"size_bytes": 63990960
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"current_version": "1.8.7-alpha",
|
|
||||||
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.7-alpha/archipelago-frontend-1.8.7-alpha.tar.gz",
|
|
||||||
"name": "archipelago-frontend-1.8.7-alpha.tar.gz",
|
|
||||||
"new_version": "1.8.7-alpha",
|
|
||||||
"sha256": "26b8c12b1b3e6b5c93841f3713037f65d0ea6a52401bd28888eb8d2f5dc483b5",
|
|
||||||
"size_bytes": 97779181
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"release_date": "2026-08-31",
|
|
||||||
"signature": "79332435d436e7bae1a5b0c12158b02083b681b6818ac90219021f1e39eb61a3e1039ed38ee8089006a0dec862fd92997e44a6c9ccf0be17e91a95cba5de6402",
|
|
||||||
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
|
||||||
"version": "1.8.7-alpha"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"changelog": [
|
||||||
|
"**SSH over the mesh is now a first-class setting.** Settings gains an \"SSH over mesh\" card: off by default, and when you allow it the node's mesh firewall opens port 22 — either to every mesh peer (behind an explicit \"I understand\" confirmation, because that's a real exposure) or only to the mesh addresses you list. The rule is owned by the node (the `90-ssh.nft` drop-in), so it survives upgrades and daemon reinstalls, and the card tells you up front whether sshd is running, whether it listens on IPv6 (the mesh is IPv6-only — this is what a broken attempt looks like before it happens), and whether password login is on (keys-only is the recommended pairing). From Termux on your phone, `fipssh <user>@<node-npub>` connects once the toggle is on — the npub is the durable address, and the command is shown with a copy button on the card.",
|
||||||
|
"**The App Store now lists apps — not parts of apps.** The signed catalog carries every manifest because the node's update layer needs their pins, and the store briefly listed them all: Mempool API, LND UI, Bitcoin UI, the Pine voice engines, the IndeeHub and Immich backends, the mesh router and friends. Components are hidden from the store listing (they still appear where they belong — the Services tab of My Apps, once installed), and four entries that never earned a tile are gone outright: MorphOS server (old), the Web5 DID wallet, Lightning Stack (an untracked upstream bundle — LND covers the need), and CryptPad (never tested).",
|
||||||
|
"**App icons now persist everywhere, in the proper container style.** Two fixes: installed apps render the icon from their own manifest — Cuprate no longer falls back to the generic A-mark on its Services tile — and the store grids (the Discover page) apply the same icon container treatment (backdrop, border, shadow) as My Apps, the detail pages, and Home. Manifest-declared UI apps also classify correctly again: Alby Hub installs into My Apps with a working tile, not into Services, because a probe miss no longer buries an app the manifest itself says has a frontend.",
|
||||||
|
"**Installing from the store keeps you on the store page.** The install progress lives on the tile itself and the app appears in My Apps when it lands — no more being yanked to My Apps mid-browse."
|
||||||
|
],
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"current_version": "1.8.8-alpha",
|
||||||
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.8-alpha/archipelago",
|
||||||
|
"name": "archipelago",
|
||||||
|
"new_version": "1.8.8-alpha",
|
||||||
|
"sha256": "96f39b8db6f08386200e1eab91c8444a7758526e6034100c8a33907ff9263530",
|
||||||
|
"size_bytes": 64175864
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"current_version": "1.8.8-alpha",
|
||||||
|
"download_url": "https://source.archipelago-foundation.org/lfg2025/archy/releases/download/v1.8.8-alpha/archipelago-frontend-1.8.8-alpha.tar.gz",
|
||||||
|
"name": "archipelago-frontend-1.8.8-alpha.tar.gz",
|
||||||
|
"new_version": "1.8.8-alpha",
|
||||||
|
"sha256": "7829b67edf8dec27997dd821650ed4d61aea721f802d46a8d47014f4b4246db1",
|
||||||
|
"size_bytes": 97730549
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"release_date": "2026-09-01",
|
||||||
|
"signature": "c839cbdcb356a503d87bc17f52b6e5f3a934ae1e72a891f2d21d85366f23debb224a2a40b9124bab50fe95444e01e27711690b1bc50062f40b7ed4f34e078d06",
|
||||||
|
"signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT",
|
||||||
|
"version": "1.8.8-alpha"
|
||||||
|
}
|
||||||
@@ -19,13 +19,10 @@ INTERNAL_MANIFEST_IDS = {
|
|||||||
"archy-nbxplorer",
|
"archy-nbxplorer",
|
||||||
"bitcoin-ui",
|
"bitcoin-ui",
|
||||||
"core-lightning",
|
"core-lightning",
|
||||||
"did-wallet",
|
|
||||||
"electrs-ui",
|
"electrs-ui",
|
||||||
"fips-ui",
|
"fips-ui",
|
||||||
"lightning-stack",
|
|
||||||
"lnd-ui",
|
"lnd-ui",
|
||||||
"mempool-api",
|
"mempool-api",
|
||||||
"morphos-server",
|
|
||||||
"router",
|
"router",
|
||||||
"strfry",
|
"strfry",
|
||||||
"web5-dwn",
|
"web5-dwn",
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ SINGLE = {
|
|||||||
"vaultwarden": "VAULTWARDEN_IMAGE",
|
"vaultwarden": "VAULTWARDEN_IMAGE",
|
||||||
"nextcloud": "NEXTCLOUD_IMAGE",
|
"nextcloud": "NEXTCLOUD_IMAGE",
|
||||||
"searxng": "SEARXNG_IMAGE",
|
"searxng": "SEARXNG_IMAGE",
|
||||||
"cryptpad": "CRYPTPAD_IMAGE",
|
|
||||||
"filebrowser": "FILEBROWSER_IMAGE",
|
"filebrowser": "FILEBROWSER_IMAGE",
|
||||||
"nginx-proxy-manager": "NPM_IMAGE",
|
"nginx-proxy-manager": "NPM_IMAGE",
|
||||||
"portainer": "PORTAINER_IMAGE",
|
"portainer": "PORTAINER_IMAGE",
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ NEXTCLOUD_IMAGE="$ARCHY_REGISTRY/nextcloud:29"
|
|||||||
SEARXNG_IMAGE="$ARCHY_REGISTRY/searxng:latest"
|
SEARXNG_IMAGE="$ARCHY_REGISTRY/searxng:latest"
|
||||||
# OnlyOffice removed — incompatible with rootless Podman (internal postgres/rabbitmq fail)
|
# OnlyOffice removed — incompatible with rootless Podman (internal postgres/rabbitmq fail)
|
||||||
# Replaced by CryptPad (single Node.js process, e2e encrypted)
|
# Replaced by CryptPad (single Node.js process, e2e encrypted)
|
||||||
CRYPTPAD_IMAGE="$ARCHY_REGISTRY/cryptpad:2024.12.0"
|
|
||||||
FILEBROWSER_IMAGE="$ARCHY_REGISTRY/filebrowser:v2.27.0"
|
FILEBROWSER_IMAGE="$ARCHY_REGISTRY/filebrowser:v2.27.0"
|
||||||
NPM_IMAGE="$ARCHY_REGISTRY/nginx-proxy-manager:latest"
|
NPM_IMAGE="$ARCHY_REGISTRY/nginx-proxy-manager:latest"
|
||||||
# 2.39.1 is what the fleet has actually been running via the moving :latest
|
# 2.39.1 is what the fleet has actually been running via the moving :latest
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ stage "cargo-check" timeout 580 cargo check --manifest-path core/Cargo.toml
|
|||||||
# 3600s leaves headroom; a warm target/ finishes in a fraction of it.
|
# 3600s leaves headroom; a warm target/ finishes in a fraction of it.
|
||||||
stage "cargo-test-weekly" timeout 3600 env CARGO_INCREMENTAL=0 \
|
stage "cargo-test-weekly" timeout 3600 env CARGO_INCREMENTAL=0 \
|
||||||
cargo test --manifest-path core/Cargo.toml -p archipelago -- \
|
cargo test --manifest-path core/Cargo.toml -p archipelago -- \
|
||||||
update:: lnd container::image_versions scanner drift missing_secret
|
update:: lnd container::image_versions scanner drift missing_secret collision
|
||||||
|
|
||||||
# ── Stage 4: live node smoke ─────────────────────────────────────────
|
# ── Stage 4: live node smoke ─────────────────────────────────────────
|
||||||
if [[ $LIVE -eq 1 ]]; then
|
if [[ $LIVE -eq 1 ]]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user