Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
// The Pithos control server: one JSON API + the static UI. Every host (web
// console, desktop window, Theseus tab) runs exactly this and points a view
// at it, so features are written once.
//
// Auth: a session secret. Local hosts pass it in the first URL (?t=...);
// the web console can instead accept a password. Either way it is exchanged
// for an HttpOnly SameSite=Strict cookie, and mutating requests also need the
// x-pithos header so a cross-site form can never drive the API.
import http from 'node:http' ;
import fs from 'node:fs' ;
import path from 'node:path' ;
import crypto from 'node:crypto' ;
import { fileURLToPath } from 'node:url' ;
import { resolveConfigPath , resolveBinary } from './paths.js' ;
import { readConfig , writeConfig , ensureConfig , EDITABLE } from './config.js' ;
import { Daemon } from './daemon.js' ;
import { makeCli , CliError } from './cli.js' ;
import { LoginSession } from './login.js' ;
import { S3Client , S3Error , publicReadPolicy , readBody } from './s3.js' ;
import * as admin from './admin.js' ;
import { installS3d , S3D _VERSION , assetForPlatform } from './binary.js' ;
Pithos 0.3.4: show the Sia account, label it, switch and restore accounts
"Acting as" read like a choice of Sia account; it picks an S3 user on
this s3d, and every user shares the one Sia account the s3d is connected
to. So:
- The picker is "S3 user", with a tooltip saying all users share one Sia
account; Buckets explains what a bucket is.
- Buckets and the Overview show "Sia account: <label> (indexer)". Pithos
cannot see the account's email, so the Sia connection page lets the
user label it (kept in pithos.json beside the s3d data).
- "Switch Sia account" stops s3d and moves its data (connection, users,
keys, buckets, upload buffer) into previous-account-<stamp>/ next to
s3d.yml, never deleting it; the guide then connects another account.
It warns about uploads still waiting for Sia and offers a flush, and
refuses while another s3d serves the config.
- "Earlier Sia accounts" lists those folders with Restore, which sets the
current account aside the same way and swaps the old one back.
Also: a fresh connect now records which indexer it used, and a "not
connected" check is re-run after 30 s so a login made outside Pithos
shows up.
2026-10-03 22:57:46 +02:00
import * as accounts from './accounts.js' ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
import { readPrefs , writePrefs } from './prefs.js' ;
2026-10-04 01:04:23 +02:00
import { createAutoFlush , clampMinutes , DEFAULT _MINUTES } from './autoflush.js' ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
import { installHosted } from './hosted-routes.js' ;
2026-10-04 03:19:02 +02:00
import { accountInfo , readConnection , fingerprint } from './sia-account.js' ;
Pithos: open HTML files as a page, or read them as text
An .html file in a drive could only be read as source. It can now also be
opened as a web page in a new tab (Theseus tab, browser tab, or the system
browser from the desktop app), with the CSS, images, scripts and pages
beside it loading as they would from a website.
The page is served under a sandbox CSP without allow-same-origin, so its
scripts run in an opaque origin: they cannot read the Pithos session or
reach its API, only the files of the folder the page link covers. Links
are random, last an hour from last use (12 hours at most), and refuse
paths that leave the folder. Pages decrypt on the fly for drives hosted
on Silent Mode.
2026-10-04 03:49:23 +02:00
import { guessType } from './hosted.js' ;
Pithos: rename drives, folders and files; show free space on Sia
S3 has no rename, so files, folders and drives kept the name they were
made with. s3d copies objects in its own database (the copy points at
the same data on Sia), so a rename is a copy to the new name and a
delete of the old: quick, and it uses no extra storage. A drive rename
makes a new bucket, moves every file, rebuilds public access for the new
name (a policy names its bucket) and removes the old one. Names that
already exist are refused. Drives on Silent Mode encrypt with keys tied
to each name, so their client gets no rename until it can re-encrypt.
The drives page and Overview now show how much of the Sia account is
used and how much is free. s3d does not report it; the indexer's
GET /account does, signed with the app key s3d registered (read from its
database for this one request, BLAKE2b-256 + Ed25519 as indexd expects).
2026-10-04 03:58:21 +02:00
import { accountSpace } from './space.js' ;
Pithos: public links at navigate.st/sia for files in your own Sia account
A share link from s3d only opened where s3d is reachable, which on a
default setup is this computer. Pithos now makes a Sia shared-object URL
for the file the way the Sia SDK does (the indexer's sealed object, its
data key opened with a key derived from s3d's app key) and registers it
at navigate.st, which streams the file from Sia hosts for anyone, with
seeking, even with this computer off. A shared-object URL reads that one
object and nothing else in the account. Files still waiting on this
computer are uploaded first. A folder gets a page with its files and a
player. Settings lists the links, each one can be switched off.
sia-share is the navigate.st side, in Go on the Sia SDK: it keeps short
ids, checks every URL against an allowed indexer, serves files sandboxed
so a shared page cannot run script on navigate.st, and caps sign-ups per
address and downloads per link per day. Host reads are paid by its own
app on Silent Mode's indexer account.
2026-10-04 16:45:07 +02:00
import { createShareUrl } from './sia-share.js' ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const HERE = path . dirname ( fileURLToPath ( import . meta . url ) ) ;
const UI _DIR = path . join ( HERE , '..' , 'ui' ) ;
const MIME = {
'.html' : 'text/html; charset=utf-8' , '.js' : 'text/javascript; charset=utf-8' , '.css' : 'text/css; charset=utf-8' ,
'.svg' : 'image/svg+xml' , '.png' : 'image/png' , '.ico' : 'image/x-icon' , '.json' : 'application/json' ,
'.woff2' : 'font/woff2' ,
} ;
class HttpError extends Error {
constructor ( status , message ) { super ( message ) ; this . status = status ; }
}
export async function createPithos ( opts = { } ) {
const {
host = '127.0.0.1' ,
port = 0 ,
configFile : configOpt ,
binary : binaryOpt ,
binDir , // where installS3d puts the pinned release
password , // web console: optional login password
allowedHosts = [ ] , // extra Host header values (reverse proxy names)
hostName = 'web' , // 'web' | 'desktop' | 'theseus' — UI tweaks only
openExternal , // (url) => void, for hosts that can open the system browser
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
showPanel , // () => void, for hosts with a side panel to switch back to (Theseus)
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
secrets , // hosted identity + encryption keys; the Theseus vault, else a local key file
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
} = opts ;
const configFile = resolveConfigPath ( configOpt ) ;
const daemon = new Daemon ( { configFile , binary : resolveBinary ( binaryOpt , binDir ) } ) ;
const cli = makeCli ( daemon ) ;
2026-10-03 20:23:42 +02:00
const login = new LoginSession ( daemon , {
registration : ( ) => cli . registration ( ) ,
// Hosts without node-pty (the Theseus add-on) fetch it here on first login.
ptyDir : binDir ? path . join ( binDir , '..' , 'pty' ) : null ,
} ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const secret = crypto . randomBytes ( 24 ) . toString ( 'base64url' ) ;
const sessions = new Set ( ) ;
2026-10-03 19:34:53 +02:00
// One-time download links: id -> { path, exp }. Lets a host hand a file
// to its own download manager, which carries no session cookie.
const tickets = new Map ( ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const subscribers = new Set ( ) ;
let versionCache = null ;
2026-10-04 02:38:19 +02:00
// Pithos's own version: addon.json in the Theseus add-on, package.json elsewhere.
const pithosVersion = ( ( ) => {
for ( const f of [ '../addon.json' , '../package.json' ] ) {
try { const v = JSON . parse ( fs . readFileSync ( new URL ( f , import . meta . url ) , 'utf8' ) ) . version ; if ( v ) return v ; } catch { }
}
return null ;
} ) ( ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
let hosted = null ; // "On Silent Mode", installed below
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const broadcast = ( event , data ) => {
const msg = ` event: ${ event } \n data: ${ JSON . stringify ( data ) } \n \n ` ;
for ( const res of subscribers ) res . write ( msg ) ;
} ;
daemon . on ( 'status' , ( s ) => {
// A registration done outside Pithos (s3d login by hand) shows up on the next start.
if ( s . state === 'starting' && ! regCache ? . registered ) regCache = null ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
// While drives are hosted the UI follows the server's s3d instead.
if ( ! hosted ? . isHosted ( ) ) broadcast ( 'status' , s ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
} ) ;
daemon . on ( 'log' , ( l ) => broadcast ( 'log' , l ) ) ;
login . on ( 'state' , ( s ) => {
if ( s . state === 'done' || s . state === 'already' ) regCache = { registered : true , indexerUrl : s . indexerUrl || regCache ? . indexerUrl } ;
broadcast ( 'login' , s ) ;
} ) ;
// Registration costs an s3d process run, so it is cached and only
// recomputed when something that could change it happens.
let regCache = null ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
let lastAutoTry = 0 ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
async function registration ( ) {
Pithos 0.3.4: show the Sia account, label it, switch and restore accounts
"Acting as" read like a choice of Sia account; it picks an S3 user on
this s3d, and every user shares the one Sia account the s3d is connected
to. So:
- The picker is "S3 user", with a tooltip saying all users share one Sia
account; Buckets explains what a bucket is.
- Buckets and the Overview show "Sia account: <label> (indexer)". Pithos
cannot see the account's email, so the Sia connection page lets the
user label it (kept in pithos.json beside the s3d data).
- "Switch Sia account" stops s3d and moves its data (connection, users,
keys, buckets, upload buffer) into previous-account-<stamp>/ next to
s3d.yml, never deleting it; the guide then connects another account.
It warns about uploads still waiting for Sia and offers a flush, and
refuses while another s3d serves the config.
- "Earlier Sia accounts" lists those folders with Restore, which sets the
current account aside the same way and swaps the old one back.
Also: a fresh connect now records which indexer it used, and a "not
connected" check is re-run after 30 s so a login made outside Pithos
shows up.
2026-10-03 22:57:46 +02:00
// A "not connected" answer is re-checked after 30 s: the connection can
// be made outside Pithos (s3d login by hand, another Pithos).
if ( regCache && ! regCache . registered && Date . now ( ) - ( regCache . at || 0 ) > 30_000 ) regCache = null ;
if ( ! regCache && daemon . binary ) {
const r = await cli . registration ( ) . catch ( ( ) => null ) ;
regCache = r ? { ... r , at : Date . now ( ) } : null ;
}
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
return regCache ;
}
function cfg ( ) { return readConfig ( configFile ) ; }
async function version ( ) {
if ( ! daemon . binary ) return null ;
if ( versionCache ? . binary === daemon . binary ) return versionCache . info ;
try {
const info = await cli . version ( ) ;
versionCache = { binary : daemon . binary , info } ;
return info ;
} catch ( e ) {
return { version : null , error : e . message } ;
}
}
// An s3d we did not start (a service, or one run by hand) that answers on
// the configured admin address.
async function probeExternal ( c ) {
if ( daemon . child || ! c . adminPassword ) return false ;
try { await admin . uploadStats ( c ) ; return true ; } catch { return false ; }
}
async function s3For ( user ) {
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
if ( hosted . isHosted ( ) ) return hosted . s3For ( user ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const keys = await cli . listKeys ( user ) ;
if ( ! keys . length ) throw new HttpError ( 409 , ` user " ${ user } " has no access keys yet - create one first ` ) ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
const decrypting = await hosted . s3For ( user , keys ) ;
if ( decrypting ) return decrypting ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const c = cfg ( ) ;
return new S3Client ( { endpoint : ` http:// ${ c . apiAddress } ` , accessKeyId : keys [ 0 ] . accessKeyId , secretKey : keys [ 0 ] . secretKey } ) ;
}
// ---- routing ---------------------------------------------------------
const routes = [ ] ;
const route = ( method , pattern , handler ) => {
const keys = [ ] ;
const re = new RegExp ( '^' + pattern . replace ( /:(\w+)/g , ( _ , k ) => { keys . push ( k ) ; return '([^/]+)' ; } ) + '$' ) ;
routes . push ( { method , re , keys , handler } ) ;
} ;
route ( 'GET' , '/api/status' , async ( ) => {
const c = cfg ( ) ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
// Backstop for the start-up autostart: opening Pithos also brings s3d
// back if the user wants it running (at most every 30 s).
if ( ! daemon . child && daemon . state !== 'starting' && Date . now ( ) - lastAutoTry > 30_000 && readPrefs ( configFile ) . autostart === true ) {
lastAutoTry = Date . now ( ) ;
autostart ( 'Pithos was opened' ) ;
}
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const v = await version ( ) ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
const local = {
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
host : hostName ,
2026-10-04 02:38:19 +02:00
pithos : pithosVersion ,
2026-10-04 22:02:14 +02:00
// electron-builder's portable exe sets this; updates then fetch the portable build.
portable : hostName === 'desktop' && ! ! process . env . PORTABLE _EXECUTABLE _FILE ,
2026-10-04 22:49:46 +02:00
platform : process . platform ,
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
daemon : daemon . status ( ) ,
external : await probeExternal ( c ) ,
s3d : { ... v , pinned : S3D _VERSION , installable : ! ! assetForPlatform ( ) , outdated : ! ! ( v ? . version && cmpVersion ( v . version , S3D _VERSION ) < 0 ) } ,
config : { file : c . file , exists : c . exists , apiAddress : c . apiAddress , adminAddress : c . adminAddress , directory : c . directory , https : c . data . apiHTTPSAddress || null } ,
login : login . snapshot ( ) ,
registration : await registration ( ) ,
} ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
return hosted . isHosted ( ) ? hosted . hostedStatus ( local ) : local ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
} ) ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
// Start and Stop also record what the user wants, so s3d comes back by
// itself after Theseus restarts or Pithos is updated.
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
route ( 'POST' , '/api/daemon/start' , async ( ) => {
ensureConfig ( configFile ) ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
writePrefs ( configFile , { autostart : true } ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
return daemon . start ( ) ;
} ) ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
route ( 'POST' , '/api/daemon/stop' , ( ) => { writePrefs ( configFile , { autostart : false } ) ; return daemon . stop ( ) ; } ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
route ( 'POST' , '/api/daemon/restart' , async ( ) => {
await daemon . stop ( ) ;
ensureConfig ( configFile ) ;
return daemon . start ( ) ;
} ) ;
route ( 'GET' , '/api/logs' , ( req , url ) => daemon . logsSince ( Number ( url . searchParams . get ( 'since' ) || 0 ) ) ) ;
route ( 'POST' , '/api/binary/install' , async ( ) => {
if ( daemon . child ) throw new HttpError ( 409 , 'stop s3d before replacing its binary' ) ;
if ( ! binDir ) throw new HttpError ( 400 , 'this host has no writable bin folder' ) ;
const target = await installS3d ( binDir , ( p ) => broadcast ( 'install' , p ) ) ;
daemon . binary = target ;
versionCache = null ;
regCache = null ;
return { binary : target , version : await version ( ) } ;
} ) ;
route ( 'GET' , '/api/config' , ( ) => {
const c = cfg ( ) ;
const values = { } ;
for ( const k of EDITABLE ) values [ k ] = getIn ( c . data , k . split ( '.' ) ) ? ? null ;
return { file : c . file , exists : c . exists , values } ;
} ) ;
route ( 'PUT' , '/api/config' , async ( req ) => {
const patch = await jsonBody ( req ) ;
const c = writeConfig ( configFile , patch ) ;
if ( 'directory' in patch ) regCache = null ;
return { file : c . file , restartNeeded : ! ! daemon . child } ;
} ) ;
// Login (indexer registration)
route ( 'GET' , '/api/login' , ( ) => login . snapshot ( ) ) ;
route ( 'POST' , '/api/login' , async ( req ) => {
const { indexerUrl , phrase } = await jsonBody ( req ) ;
if ( phrase && phrase . trim ( ) . split ( /\s+/ ) . length !== 12 ) throw new HttpError ( 400 , 'a recovery phrase is 12 words' ) ;
if ( indexerUrl && ! /^https?:\/\/[^\s]+$/ . test ( indexerUrl ) ) throw new HttpError ( 400 , 'indexer URL must be http(s)' ) ;
ensureConfig ( configFile ) ;
return login . start ( { indexerUrl : indexerUrl ? . trim ( ) , phrase : phrase ? . trim ( ) . toLowerCase ( ) . split ( /\s+/ ) . join ( ' ' ) } ) ;
} ) ;
route ( 'POST' , '/api/login/confirm' , ( ) => { login . confirmGenerated ( ) ; return login . snapshot ( ) ; } ) ;
route ( 'POST' , '/api/login/cancel' , ( ) => { login . cancel ( ) ; return login . snapshot ( ) ; } ) ;
route ( 'POST' , '/api/open-external' , async ( req ) => {
const { url } = await jsonBody ( req ) ;
if ( ! openExternal ) throw new HttpError ( 400 , 'not supported by this host' ) ;
if ( ! /^https:\/\// . test ( url ) ) throw new HttpError ( 400 , 'only https links' ) ;
openExternal ( url ) ;
return { ok : true } ;
} ) ;
// Users & keys
route ( 'GET' , '/api/users' , async ( ) => {
const [ users , keys ] = await Promise . all ( [ cli . listUsers ( ) , cli . listKeys ( ) ] ) ;
return users . map ( ( name ) => ( { name , keys : keys . filter ( ( k ) => k . user === name ) . map ( ( k ) => k . accessKeyId ) } ) ) ;
} ) ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
// Every user gets a bucket of their own (named after them unless the
// caller picks a name), so there is somewhere to put files straight away.
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
route ( 'POST' , '/api/users' , async ( req ) => {
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
const { name , withKey = true , bucket } = await jsonBody ( req ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
await cli . createUser ( name ) ;
const key = withKey ? await cli . createKey ( name ) : null ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
let created = null ;
let bucketError = null ;
if ( key && bucket !== false && daemon . state === 'running' ) {
const client = new S3Client ( { endpoint : ` http:// ${ cfg ( ) . apiAddress } ` , accessKeyId : key . accessKeyId , secretKey : key . secretKey } ) ;
const base = bucket ? String ( bucket ) : bucketNameFor ( name ) ;
for ( let i = 0 ; i < 5 && ! created ; i ++ ) {
const candidate = i ? ` ${ base . slice ( 0 , 60 ) } - ${ i + 1 } ` : base ;
try { await client . createBucket ( candidate ) ; created = candidate ; }
catch ( e ) {
if ( e . code === 'BucketAlreadyExists' || e . code === 'BucketAlreadyOwnedByYou' ) continue ;
bucketError = e . message ;
break ;
}
}
}
return { name , key , bucket : created , bucketError } ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
} ) ;
route ( 'DELETE' , '/api/users/:name' , async ( req , url , p ) => { await cli . deleteUser ( p . name ) ; return { ok : true } ; } ) ;
route ( 'GET' , '/api/keys' , ( req , url ) => cli . listKeys ( url . searchParams . get ( 'user' ) || undefined ) ) ;
route ( 'POST' , '/api/keys' , async ( req ) => {
const { user , accessKey , secretKey } = await jsonBody ( req ) ;
return cli . createKey ( user , { accessKey , secretKey } ) ;
} ) ;
route ( 'DELETE' , '/api/keys/:id' , async ( req , url , p ) => { await cli . deleteKey ( p . id ) ; return { ok : true } ; } ) ;
// Upload pipeline
Pithos 0.3.4: show the Sia account, label it, switch and restore accounts
"Acting as" read like a choice of Sia account; it picks an S3 user on
this s3d, and every user shares the one Sia account the s3d is connected
to. So:
- The picker is "S3 user", with a tooltip saying all users share one Sia
account; Buckets explains what a bucket is.
- Buckets and the Overview show "Sia account: <label> (indexer)". Pithos
cannot see the account's email, so the Sia connection page lets the
user label it (kept in pithos.json beside the s3d data).
- "Switch Sia account" stops s3d and moves its data (connection, users,
keys, buckets, upload buffer) into previous-account-<stamp>/ next to
s3d.yml, never deleting it; the guide then connects another account.
It warns about uploads still waiting for Sia and offers a flush, and
refuses while another s3d serves the config.
- "Earlier Sia accounts" lists those folders with Restore, which sets the
current account aside the same way and swaps the old one back.
Also: a fresh connect now records which indexer it used, and a "not
connected" check is re-run after 30 s so a login made outside Pithos
shows up.
2026-10-03 22:57:46 +02:00
// Sia account: which one this s3d uses, a user-set label (Pithos cannot
// see the account's email), switching to another and restoring an old one.
2026-10-04 03:19:02 +02:00
// Which Sia account this is, as the indexer sees it (public key, storage,
// last use), so it can be matched to an app on the sia.storage dashboard.
// Asked at most once a minute; ?fresh=1 skips the cache.
let siaCache = null ;
async function siaAccount ( dir , fresh ) {
if ( ! fresh && siaCache && siaCache . dir === dir && Date . now ( ) - siaCache . at < 60_000 ) return siaCache . info ;
let info ;
try { info = await accountInfo ( dir ) ; } catch ( e ) { info = { error : e . message } ; }
siaCache = { dir , at : Date . now ( ) , info } ;
return info ;
}
// An archive's key is read from its own s3d.db, without asking anyone.
async function archiveKey ( dir ) {
try {
const c = await readConnection ( dir ) ;
return c ? . appKey ? fingerprint ( c . appKey . subarray ( 32 ) . toString ( 'hex' ) ) : null ;
} catch { return null ; }
}
route ( 'GET' , '/api/account' , async ( req , url ) => {
Pithos 0.3.4: show the Sia account, label it, switch and restore accounts
"Acting as" read like a choice of Sia account; it picks an S3 user on
this s3d, and every user shares the one Sia account the s3d is connected
to. So:
- The picker is "S3 user", with a tooltip saying all users share one Sia
account; Buckets explains what a bucket is.
- Buckets and the Overview show "Sia account: <label> (indexer)". Pithos
cannot see the account's email, so the Sia connection page lets the
user label it (kept in pithos.json beside the s3d data).
- "Switch Sia account" stops s3d and moves its data (connection, users,
keys, buckets, upload buffer) into previous-account-<stamp>/ next to
s3d.yml, never deleting it; the guide then connects another account.
It warns about uploads still waiting for Sia and offers a flush, and
refuses while another s3d serves the config.
- "Earlier Sia accounts" lists those folders with Restore, which sets the
current account aside the same way and swaps the old one back.
Also: a fresh connect now records which indexer it used, and a "not
connected" check is re-run after 30 s so a login made outside Pithos
shows up.
2026-10-03 22:57:46 +02:00
const c = cfg ( ) ;
const reg = await registration ( ) ;
2026-10-04 03:19:02 +02:00
const archives = accounts . listArchives ( c . directory ) ;
for ( const a of archives ) a . fingerprint = await archiveKey ( path . join ( c . directory , a . name ) ) ;
Pithos 0.3.4: show the Sia account, label it, switch and restore accounts
"Acting as" read like a choice of Sia account; it picks an S3 user on
this s3d, and every user shares the one Sia account the s3d is connected
to. So:
- The picker is "S3 user", with a tooltip saying all users share one Sia
account; Buckets explains what a bucket is.
- Buckets and the Overview show "Sia account: <label> (indexer)". Pithos
cannot see the account's email, so the Sia connection page lets the
user label it (kept in pithos.json beside the s3d data).
- "Switch Sia account" stops s3d and moves its data (connection, users,
keys, buckets, upload buffer) into previous-account-<stamp>/ next to
s3d.yml, never deleting it; the guide then connects another account.
It warns about uploads still waiting for Sia and offers a flush, and
refuses while another s3d serves the config.
- "Earlier Sia accounts" lists those folders with Restore, which sets the
current account aside the same way and swaps the old one back.
Also: a fresh connect now records which indexer it used, and a "not
connected" check is re-run after 30 s so a login made outside Pithos
shows up.
2026-10-03 22:57:46 +02:00
return {
indexerUrl : reg ? . indexerUrl || null ,
registered : ! ! reg ? . registered ,
label : accounts . readLabel ( c . directory ) ,
dataDir : c . directory ,
2026-10-04 03:19:02 +02:00
sia : reg ? . registered ? await siaAccount ( c . directory , url . searchParams . get ( 'fresh' ) === '1' ) : null ,
archives ,
Pithos 0.3.4: show the Sia account, label it, switch and restore accounts
"Acting as" read like a choice of Sia account; it picks an S3 user on
this s3d, and every user shares the one Sia account the s3d is connected
to. So:
- The picker is "S3 user", with a tooltip saying all users share one Sia
account; Buckets explains what a bucket is.
- Buckets and the Overview show "Sia account: <label> (indexer)". Pithos
cannot see the account's email, so the Sia connection page lets the
user label it (kept in pithos.json beside the s3d data).
- "Switch Sia account" stops s3d and moves its data (connection, users,
keys, buckets, upload buffer) into previous-account-<stamp>/ next to
s3d.yml, never deleting it; the guide then connects another account.
It warns about uploads still waiting for Sia and offers a flush, and
refuses while another s3d serves the config.
- "Earlier Sia accounts" lists those folders with Restore, which sets the
current account aside the same way and swaps the old one back.
Also: a fresh connect now records which indexer it used, and a "not
connected" check is re-run after 30 s so a login made outside Pithos
shows up.
2026-10-03 22:57:46 +02:00
} ;
} ) ;
route ( 'PUT' , '/api/account/label' , async ( req ) => {
const { label } = await jsonBody ( req ) ;
return { label : accounts . writeLabel ( cfg ( ) . directory , label ) } ;
} ) ;
async function accountGuard ( ) {
if ( await probeExternal ( cfg ( ) ) ) throw new HttpError ( 409 , 'another s3d process is serving this config; stop it first' ) ;
if ( login . state !== 'idle' && ! [ 'done' , 'already' , 'failed' ] . includes ( login . state ) ) throw new HttpError ( 409 , 'a Sia login is in progress' ) ;
}
async function currentMeta ( ) {
const reg = await registration ( ) . catch ( ( ) => null ) ;
const users = await cli . listUsers ( ) . catch ( ( ) => [ ] ) ;
return { indexerUrl : reg ? . indexerUrl || null , users } ;
}
route ( 'POST' , '/api/account/switch' , async ( ) => {
await accountGuard ( ) ;
const meta = await currentMeta ( ) ;
await daemon . stop ( ) ;
const r = accounts . archiveCurrent ( cfg ( ) . directory , meta ) ;
regCache = null ;
login . cancel ( ) ;
return { archived : r . name , path : r . path } ;
} ) ;
route ( 'POST' , '/api/account/restore' , async ( req ) => {
const { name } = await jsonBody ( req ) ;
await accountGuard ( ) ;
const meta = await currentMeta ( ) ;
await daemon . stop ( ) ;
const r = accounts . restoreArchive ( cfg ( ) . directory , String ( name || '' ) , meta ) ;
regCache = null ;
login . cancel ( ) ;
return r ;
} ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
route ( 'GET' , '/api/stats' , ( ) => admin . uploadStats ( cfg ( ) ) ) ;
route ( 'POST' , '/api/flush' , async ( ) => { await admin . flush ( cfg ( ) ) ; return { ok : true } ; } ) ;
2026-10-04 01:04:23 +02:00
const autoFlush = createAutoFlush ( {
settings : ( ) => {
const p = readPrefs ( configFile ) ;
2026-10-04 14:39:34 +02:00
// On unless the user turned it off: files left in the buffer otherwise
// never reach Sia, and nobody expects to have to push them.
return { enabled : p . autoFlush !== false , minutes : p . autoFlushMinutes ? ? DEFAULT _MINUTES } ;
2026-10-04 01:04:23 +02:00
} ,
// Only an s3d that is up can be asked; a stopped one just means no flush.
stats : ( ) => ( daemon . state === 'starting' || daemon . state === 'stopping'
? Promise . reject ( new Error ( 'busy' ) ) : admin . uploadStats ( cfg ( ) ) ) ,
flush : ( ) => admin . flush ( cfg ( ) ) ,
log : ( line ) => daemon . pushLog ( 'sys' , line ) ,
} ) ;
route ( 'GET' , '/api/autoflush' , ( ) => autoFlush . status ( ) ) ;
route ( 'PUT' , '/api/autoflush' , async ( req ) => {
const body = await jsonBody ( req ) ;
const patch = { } ;
if ( body . enabled !== undefined ) patch . autoFlush = body . enabled === true ;
if ( body . minutes !== undefined ) patch . autoFlushMinutes = clampMinutes ( body . minutes ) ;
writePrefs ( configFile , patch ) ;
return autoFlush . status ( ) ;
} ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
// Buckets & objects, acting as one s3d user.
route ( 'GET' , '/api/s3/:user/buckets' , async ( req , url , p ) => ( await s3For ( p . user ) ) . listBuckets ( ) ) ;
route ( 'POST' , '/api/s3/:user/buckets' , async ( req , url , p ) => {
const { name } = await jsonBody ( req ) ;
if ( ! /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/ . test ( name || '' ) ) throw new HttpError ( 400 , 'bucket names are 3-63 chars: lowercase letters, digits, dots, hyphens' ) ;
await ( await s3For ( p . user ) ) . createBucket ( name ) ;
return { name } ;
} ) ;
route ( 'DELETE' , '/api/s3/:user/buckets/:bucket' , async ( req , url , p ) => {
await ( await s3For ( p . user ) ) . deleteBucket ( p . bucket ) ;
return { ok : true } ;
} ) ;
route ( 'GET' , '/api/s3/:user/buckets/:bucket/objects' , async ( req , url , p ) => {
const q = url . searchParams ;
return ( await s3For ( p . user ) ) . listObjects ( p . bucket , { prefix : q . get ( 'prefix' ) || '' , token : q . get ( 'token' ) || undefined } ) ;
} ) ;
route ( 'PUT' , '/api/s3/:user/buckets/:bucket/object' , async ( req , url , p ) => {
const key = requireKey ( url ) ;
const len = req . headers [ 'content-length' ] ;
if ( len == null ) throw new HttpError ( 411 , 'Content-Length required' ) ;
if ( Number ( len ) > 5 * 1024 * * 3 ) throw new HttpError ( 413 , 'single uploads are limited to 5 GiB' ) ;
await ( await s3For ( p . user ) ) . putObject ( p . bucket , key , req , { contentType : req . headers [ 'content-type' ] , contentLength : len } ) ;
return { key } ;
} ) ;
route ( 'GET' , '/api/s3/:user/buckets/:bucket/object' , async ( req , url , p , res ) => {
const key = requireKey ( url ) ;
const up = await ( await s3For ( p . user ) ) . getObject ( p . bucket , key , { range : req . headers . range } ) ;
if ( up . statusCode >= 300 && up . statusCode !== 304 ) {
throw new HttpError ( up . statusCode , ( await readBody ( up ) ) . match ( /<Message>(.*?)<\/Message>/ ) ? . [ 1 ] || ` HTTP ${ up . statusCode } ` ) ;
}
const headers = { } ;
for ( const h of [ 'content-type' , 'content-length' , 'content-range' , 'accept-ranges' , 'etag' , 'last-modified' ] ) if ( up . headers [ h ] ) headers [ h ] = up . headers [ h ] ;
const name = key . split ( '/' ) . pop ( ) || 'download' ;
headers [ 'content-disposition' ] = ` ${ url . searchParams . get ( 'inline' ) ? 'inline' : 'attachment' } ; filename*=UTF-8'' ${ encodeURIComponent ( name ) } ` ;
headers [ 'x-content-type-options' ] = 'nosniff' ;
// Objects are user content: never let them run script on this origin.
headers [ 'content-security-policy' ] = "sandbox; default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'" ;
res . writeHead ( up . statusCode , headers ) ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
// A decryption failure mid-file must not look like a complete download.
up . on ( 'error' , ( ) => res . destroy ( ) ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
up . pipe ( res ) ;
return STREAMED ;
} ) ;
route ( 'DELETE' , '/api/s3/:user/buckets/:bucket/object' , async ( req , url , p ) => {
await ( await s3For ( p . user ) ) . deleteObject ( p . bucket , requireKey ( url ) ) ;
return { ok : true } ;
} ) ;
route ( 'DELETE' , '/api/s3/:user/buckets/:bucket/prefix' , async ( req , url , p ) => {
const prefix = url . searchParams . get ( 'prefix' ) ;
if ( ! prefix ) throw new HttpError ( 400 , 'prefix required' ) ;
return { deleted : await ( await s3For ( p . user ) ) . deletePrefix ( p . bucket , prefix ) } ;
} ) ;
Pithos: rename drives, folders and files; show free space on Sia
S3 has no rename, so files, folders and drives kept the name they were
made with. s3d copies objects in its own database (the copy points at
the same data on Sia), so a rename is a copy to the new name and a
delete of the old: quick, and it uses no extra storage. A drive rename
makes a new bucket, moves every file, rebuilds public access for the new
name (a policy names its bucket) and removes the old one. Names that
already exist are refused. Drives on Silent Mode encrypt with keys tied
to each name, so their client gets no rename until it can re-encrypt.
The drives page and Overview now show how much of the Sia account is
used and how much is free. s3d does not report it; the indexer's
GET /account does, signed with the app key s3d registered (read from its
database for this one request, BLAKE2b-256 + Ed25519 as indexd expects).
2026-10-04 03:58:21 +02:00
// ---- renames: files, folders, drives -----------------------------------
// Server-side copies, so they are quick and use no extra Sia storage.
2026-10-04 04:01:36 +02:00
// Every client Pithos makes can rename; the check stays for any that
// cannot (encrypted names and contents are tied to their path, so a raw
// server-side copy would break them).
Pithos: rename drives, folders and files; show free space on Sia
S3 has no rename, so files, folders and drives kept the name they were
made with. s3d copies objects in its own database (the copy points at
the same data on Sia), so a rename is a copy to the new name and a
delete of the old: quick, and it uses no extra storage. A drive rename
makes a new bucket, moves every file, rebuilds public access for the new
name (a policy names its bucket) and removes the old one. Names that
already exist are refused. Drives on Silent Mode encrypt with keys tied
to each name, so their client gets no rename until it can re-encrypt.
The drives page and Overview now show how much of the Sia account is
used and how much is free. s3d does not report it; the indexer's
GET /account does, signed with the app key s3d registered (read from its
database for this one request, BLAKE2b-256 + Ed25519 as indexd expects).
2026-10-04 03:58:21 +02:00
async function renamer ( user ) {
const s3 = await s3For ( user ) ;
if ( typeof s3 . renamePrefix !== 'function' ) throw new HttpError ( 409 , 'Renaming is not available for drives on Silent Mode yet.' ) ;
return s3 ;
}
const badName = ( n ) => ! n || n . includes ( '/' ) || n === '.' || n === '..' ;
async function exists ( s3 , bucket , prefix ) {
const page = await s3 . listObjects ( bucket , { prefix , delimiter : '' , max : 1 } ) ;
return page . objects . some ( ( o ) => o . key === prefix || prefix . endsWith ( '/' ) ) ;
}
// A file: { from: "<key>", name: "<new name>" } in the same folder.
route ( 'POST' , '/api/s3/:user/buckets/:bucket/rename-object' , async ( req , url , p ) => {
const { from , name } = await jsonBody ( req ) ;
if ( ! from || from . endsWith ( '/' ) || badName ( name ) ) throw new HttpError ( 400 , 'a file and a new name (without "/") are required' ) ;
const to = from . slice ( 0 , from . lastIndexOf ( '/' ) + 1 ) + name ;
if ( to === from ) return { key : to } ;
const s3 = await renamer ( p . user ) ;
if ( await exists ( s3 , p . bucket , to ) ) throw new HttpError ( 409 , ` " ${ name } " already exists here ` ) ;
await s3 . renameObject ( p . bucket , from , to ) ;
return { key : to } ;
} ) ;
// A folder: { from: "<prefix>/", name: "<new name>" } beside it.
route ( 'POST' , '/api/s3/:user/buckets/:bucket/rename-prefix' , async ( req , url , p ) => {
const { from , name } = await jsonBody ( req ) ;
if ( ! from || ! from . endsWith ( '/' ) || badName ( name ) ) throw new HttpError ( 400 , 'a folder and a new name (without "/") are required' ) ;
const parent = from . slice ( 0 , from . slice ( 0 , - 1 ) . lastIndexOf ( '/' ) + 1 ) ;
const to = ` ${ parent } ${ name } / ` ;
if ( to === from ) return { prefix : to , moved : 0 } ;
const s3 = await renamer ( p . user ) ;
if ( await exists ( s3 , p . bucket , to ) ) throw new HttpError ( 409 , ` a folder " ${ name } " already exists here ` ) ;
return { prefix : to , moved : await s3 . renamePrefix ( p . bucket , from , to ) } ;
} ) ;
// A drive: a new bucket gets every file and the same access, then the old
// one goes. S3 apps that use the old name need the new one.
route ( 'POST' , '/api/s3/:user/buckets/:bucket/rename' , async ( req , url , p ) => {
const { name } = await jsonBody ( req ) ;
if ( ! /^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/ . test ( String ( name || '' ) ) ) throw new HttpError ( 400 , 'drive names are 3-63 lowercase letters, digits, dots and hyphens' ) ;
if ( name === p . bucket ) return { name } ;
const s3 = await renamer ( p . user ) ;
if ( ( await s3 . listBuckets ( ) ) . some ( ( b ) => b . name === name ) ) throw new HttpError ( 409 , ` a drive " ${ name } " already exists ` ) ;
await s3 . createBucket ( name ) ;
// Public access carries over, rebuilt for the new name (a policy names its bucket).
const mode = describePolicy ( await s3 . getPolicy ( p . bucket ) . catch ( ( ) => null ) ) ;
const moved = await s3 . renamePrefix ( p . bucket , '' , '' , name ) ;
if ( mode === 'read' || mode === 'read-list' ) await s3 . putPolicy ( name , publicReadPolicy ( name , { list : mode === 'read-list' } ) ) ;
await s3 . deleteBucket ( p . bucket ) ;
return { name , moved } ;
} ) ;
Pithos: public links at navigate.st/sia for files in your own Sia account
A share link from s3d only opened where s3d is reachable, which on a
default setup is this computer. Pithos now makes a Sia shared-object URL
for the file the way the Sia SDK does (the indexer's sealed object, its
data key opened with a key derived from s3d's app key) and registers it
at navigate.st, which streams the file from Sia hosts for anyone, with
seeking, even with this computer off. A shared-object URL reads that one
object and nothing else in the account. Files still waiting on this
computer are uploaded first. A folder gets a page with its files and a
player. Settings lists the links, each one can be switched off.
sia-share is the navigate.st side, in Go on the Sia SDK: it keeps short
ids, checks every URL against an allowed indexer, serves files sandboxed
so a shared page cannot run script on navigate.st, and caps sign-ups per
address and downloads per link per day. Host reads are paid by its own
app on Silent Mode's indexer account.
2026-10-04 16:45:07 +02:00
// ---- public links: navigate.st/sia/<id>/<name> -----------------------------------
// A file in your own Sia account gets a Sia shared-object URL (it reads that
// one file and nothing else), registered at navigate.st, which streams it
// from Sia for anyone. Files still waiting on this computer go up first.
// The delete tokens stay here, beside s3d.yml, so links can be switched off.
const SIA _LINKS _API = process . env . PITHOS _SIA _LINKS _API || 'https://navigate.st/api/sia/links' ;
const siaLinksFile = ( ) => path . join ( path . dirname ( configFile ) , 'pithos-sia-links.json' ) ;
const readSiaLinks = ( ) => { try { return JSON . parse ( fs . readFileSync ( siaLinksFile ( ) , 'utf8' ) ) ; } catch { return [ ] ; } } ;
const writeSiaLinks = ( list ) => fs . writeFileSync ( siaLinksFile ( ) , JSON . stringify ( list , null , 2 ) , { mode : 0o600 } ) ;
async function shareUrlFor ( bucket , key , until ) {
const dir = cfg ( ) . directory ;
try { return await createShareUrl ( dir , bucket , key , until ) ; }
catch ( e ) {
if ( e . code !== 'pending' ) throw new HttpError ( e . status || 502 , e . message ) ;
await admin . flush ( cfg ( ) ) ; // send what is waiting, then try once more
try { return await createShareUrl ( dir , bucket , key , until ) ; }
catch ( e2 ) { throw new HttpError ( e2 . status || 502 , e2 . code === 'pending' ? 'This file is still uploading to Sia. Try again in a minute.' : e2 . message ) ; }
}
}
route ( 'POST' , '/api/s3/:user/buckets/:bucket/sia-link' , async ( req , url , p ) => {
if ( hosted . isHosted ( ) ) throw new HttpError ( 409 , 'Drives on Silent Mode share through Share… (navigate.st/s3).' ) ;
const { key , prefix , days = 365 } = await jsonBody ( req ) ;
const until = Math . floor ( Date . now ( ) / 1000 ) + Math . min ( 3650 , Math . max ( 1 , Number ( days ) || 365 ) ) * 86400 ;
let body ;
if ( prefix ) {
if ( ! prefix . endsWith ( '/' ) ) throw new HttpError ( 400 , 'a folder ends with /' ) ;
const s3 = await s3For ( p . user ) ;
const files = [ ] ;
let token ;
do {
const page = await s3 . listObjects ( p . bucket , { prefix , token } ) ;
for ( const o of page . objects ) if ( o . key !== prefix && o . size > 0 ) files . push ( o . key ) ;
token = page . truncated ? page . nextToken : null ;
} while ( token && files . length < 1000 ) ;
if ( ! files . length ) throw new HttpError ( 400 , 'This folder has no files to share (files in sub-folders are not included).' ) ;
const out = [ ] ;
for ( const k of files ) out . push ( { name : k . slice ( prefix . length ) , url : ( await shareUrlFor ( p . bucket , k , until ) ) . url } ) ;
body = { name : prefix . slice ( 0 , - 1 ) . split ( '/' ) . pop ( ) , folder : true , files : out } ;
} else {
if ( ! key || key . endsWith ( '/' ) ) throw new HttpError ( 400 , 'a file is required' ) ;
body = { name : key . split ( '/' ) . pop ( ) , url : ( await shareUrlFor ( p . bucket , key , until ) ) . url } ;
}
const res = await fetch ( SIA _LINKS _API , { method : 'POST' , headers : { 'content-type' : 'application/json' } , body : JSON . stringify ( body ) , signal : AbortSignal . timeout ( 180_000 ) } ) ;
const out = await res . json ( ) . catch ( ( ) => ( { } ) ) ;
if ( ! res . ok ) throw new HttpError ( res . status === 429 ? 429 : 502 , out . error || ` navigate.st answered ${ res . status } ` ) ;
const list = readSiaLinks ( ) ;
list . unshift ( { id : out . id , url : out . url , deleteToken : out . deleteToken , user : p . user , bucket : p . bucket , key : key || null , prefix : prefix || null , until , created : Date . now ( ) , files : body . files ? body . files . length : 1 } ) ;
writeSiaLinks ( list ) ;
return { id : out . id , url : out . url , until } ;
} ) ;
route ( 'GET' , '/api/sia-links' , ( ) => readSiaLinks ( ) . map ( ( { deleteToken , ... l } ) => l ) ) ;
route ( 'DELETE' , '/api/sia-links/:id' , async ( req , url , p ) => {
const list = readSiaLinks ( ) ;
const l = list . find ( ( x ) => x . id === p . id ) ;
if ( ! l ) throw new HttpError ( 404 , 'no such link' ) ;
const res = await fetch ( ` ${ SIA _LINKS _API } / ${ encodeURIComponent ( l . id ) } ` , { method : 'DELETE' , headers : { 'x-delete-token' : l . deleteToken } , signal : AbortSignal . timeout ( 30_000 ) } ) ;
if ( ! res . ok && res . status !== 404 ) throw new HttpError ( 502 , ` navigate.st answered ${ res . status } ` ) ;
writeSiaLinks ( list . filter ( ( x ) => x . id !== p . id ) ) ;
return { ok : true } ;
} ) ;
Pithos: rename drives, folders and files; show free space on Sia
S3 has no rename, so files, folders and drives kept the name they were
made with. s3d copies objects in its own database (the copy points at
the same data on Sia), so a rename is a copy to the new name and a
delete of the old: quick, and it uses no extra storage. A drive rename
makes a new bucket, moves every file, rebuilds public access for the new
name (a policy names its bucket) and removes the old one. Names that
already exist are refused. Drives on Silent Mode encrypt with keys tied
to each name, so their client gets no rename until it can re-encrypt.
The drives page and Overview now show how much of the Sia account is
used and how much is free. s3d does not report it; the indexer's
GET /account does, signed with the app key s3d registered (read from its
database for this one request, BLAKE2b-256 + Ed25519 as indexd expects).
2026-10-04 03:58:21 +02:00
// ---- free space on the Sia account ----------------------------------------
// From the indexer (signed with s3d's app key). Cached for a minute.
let spaceCache = { at : 0 , value : null } ;
2026-10-04 22:02:14 +02:00
// ---- desktop updates: the newest Pithos desktop build on dl.silentmode.st ----
const RELEASES _URL = process . env . PITHOS _RELEASES _URL || 'https://dl.silentmode.st/releases-manifest.json' ;
let releaseCache = null ;
route ( 'GET' , '/api/desktop-release' , async ( req , url ) => {
if ( releaseCache && Date . now ( ) - releaseCache . at < 10 * 60_000 && url . searchParams . get ( 'fresh' ) !== '1' ) return releaseCache . body ;
const res = await fetch ( ` ${ RELEASES _URL } ?t= ${ Math . floor ( Date . now ( ) / 60000 ) } ` , { cache : 'no-store' , signal : AbortSignal . timeout ( 15_000 ) } ) . catch ( ( e ) => { throw new HttpError ( 502 , e . message ) ; } ) ;
if ( ! res . ok ) throw new HttpError ( 502 , ` the release list answered ${ res . status } ` ) ;
2026-10-04 22:49:46 +02:00
// each build is listed per platform ("win-x64", "linux-x64", "mac-…"); only ours counts
const os = { win32 : 'win' , linux : 'linux' , darwin : 'mac' } [ process . platform ] ;
const list = ( ( await res . json ( ) ) ? . releases || [ ] ) . filter ( ( e ) => e . id === 'pithos-desktop' && /^\d+\.\d+\.\d+$/ . test ( e . version ) && String ( e . platform || '' ) . startsWith ( ` ${ os } - ` ) ) ;
2026-10-04 22:02:14 +02:00
const latest = list . reduce ( ( best , e ) => ( ! best || cmpVersion ( e . version , best . version ) > 0 ? e : best ) , null ) ;
const body = { latest : latest && { version : latest . version , date : latest . date || null , changes : String ( latest . changes || '' ) . slice ( 0 , 600 ) } } ;
releaseCache = { at : Date . now ( ) , body } ;
return body ;
} ) ;
Pithos: rename drives, folders and files; show free space on Sia
S3 has no rename, so files, folders and drives kept the name they were
made with. s3d copies objects in its own database (the copy points at
the same data on Sia), so a rename is a copy to the new name and a
delete of the old: quick, and it uses no extra storage. A drive rename
makes a new bucket, moves every file, rebuilds public access for the new
name (a policy names its bucket) and removes the old one. Names that
already exist are refused. Drives on Silent Mode encrypt with keys tied
to each name, so their client gets no rename until it can re-encrypt.
The drives page and Overview now show how much of the Sia account is
used and how much is free. s3d does not report it; the indexer's
GET /account does, signed with the app key s3d registered (read from its
database for this one request, BLAKE2b-256 + Ed25519 as indexd expects).
2026-10-04 03:58:21 +02:00
route ( 'GET' , '/api/space' , async ( ) => {
2026-10-04 04:01:36 +02:00
// On Silent Mode this call is forwarded: the server's s3d answers for its own folder.
Pithos: rename drives, folders and files; show free space on Sia
S3 has no rename, so files, folders and drives kept the name they were
made with. s3d copies objects in its own database (the copy points at
the same data on Sia), so a rename is a copy to the new name and a
delete of the old: quick, and it uses no extra storage. A drive rename
makes a new bucket, moves every file, rebuilds public access for the new
name (a policy names its bucket) and removes the old one. Names that
already exist are refused. Drives on Silent Mode encrypt with keys tied
to each name, so their client gets no rename until it can re-encrypt.
The drives page and Overview now show how much of the Sia account is
used and how much is free. s3d does not report it; the indexer's
GET /account does, signed with the app key s3d registered (read from its
database for this one request, BLAKE2b-256 + Ed25519 as indexd expects).
2026-10-04 03:58:21 +02:00
if ( Date . now ( ) - spaceCache . at < 60_000 && spaceCache . value ) return spaceCache . value ;
try {
const s = await accountSpace ( cfg ( ) . directory ) ;
spaceCache = { at : Date . now ( ) , value : s ? { available : true , ... s } : { available : false , reason : 'not connected' } } ;
} catch ( e ) {
spaceCache = { at : Date . now ( ) - 45_000 , value : { available : false , reason : e . message } } ;
}
return spaceCache . value ;
} ) ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
// S3 has no folders, only names with slashes. An empty object named
// "<folder>/" is the usual marker that keeps an empty folder visible.
route ( 'PUT' , '/api/s3/:user/buckets/:bucket/folder' , async ( req , url , p ) => {
let prefix = url . searchParams . get ( 'prefix' ) || '' ;
if ( ! prefix || prefix . startsWith ( '/' ) || prefix . includes ( '//' ) ) throw new HttpError ( 400 , 'folder name required' ) ;
if ( ! prefix . endsWith ( '/' ) ) prefix += '/' ;
await ( await s3For ( p . user ) ) . putObject ( p . bucket , prefix , Buffer . alloc ( 0 ) , { contentLength : 0 } ) ;
return { prefix } ;
} ) ;
// Small text files for the viewer (first 512 KiB).
Pithos: open HTML files as a page, or read them as text
An .html file in a drive could only be read as source. It can now also be
opened as a web page in a new tab (Theseus tab, browser tab, or the system
browser from the desktop app), with the CSS, images, scripts and pages
beside it loading as they would from a website.
The page is served under a sandbox CSP without allow-same-origin, so its
scripts run in an opaque origin: they cannot read the Pithos session or
reach its API, only the files of the folder the page link covers. Links
are random, last an hour from last use (12 hours at most), and refuse
paths that leave the folder. Pages decrypt on the fly for drives hosted
on Silent Mode.
2026-10-04 03:49:23 +02:00
// HTML opened as a page. A ticket covers the folder the page sits in, so
// its relative CSS, images, scripts and links resolve; it lasts an hour from
// the last use (12 hours at most). See servePage for the sandbox.
const pages = new Map ( ) ; // id -> { user, bucket, prefix, exp, until }
route ( 'POST' , '/api/s3/:user/buckets/:bucket/page' , async ( req , url , p ) => {
const { key } = await jsonBody ( req ) ;
if ( typeof key !== 'string' || ! /\.html?$/i . test ( key ) ) throw new HttpError ( 400 , 'only .html files open as pages' ) ;
const now = Date . now ( ) ;
for ( const [ k , v ] of pages ) if ( v . exp < now ) pages . delete ( k ) ;
if ( pages . size >= 200 ) throw new HttpError ( 429 , 'too many pages open; close some and try again' ) ;
const cut = key . lastIndexOf ( '/' ) + 1 ;
const id = crypto . randomBytes ( 24 ) . toString ( 'base64url' ) ;
pages . set ( id , { user : p . user , bucket : p . bucket , prefix : key . slice ( 0 , cut ) , exp : now + 60 * 60_000 , until : now + 12 * 3600_000 } ) ;
return { path : ` /page/ ${ id } / ${ key . slice ( cut ) . split ( '/' ) . map ( encodeURIComponent ) . join ( '/' ) } ` } ;
} ) ;
// The page and its neighbours. The sandbox CSP (no allow-same-origin) gives
// the page an opaque origin: its scripts run, but requests they make to this
// server are cross-site, so they carry no session cookie and cannot pass
// the x-pithos check. They can only read files the ticket covers.
// The page loads whatever it links to (it is the user's own site); the
// sandbox is the boundary, not a source list.
const PAGE _CSP = "sandbox allow-scripts allow-forms allow-popups allow-modals allow-downloads; frame-ancestors 'self'" ;
async function servePage ( req , res , id , rest ) {
const t = pages . get ( id ) ;
const now = Date . now ( ) ;
if ( ! t || t . exp < now ) { res . writeHead ( 410 , { 'content-type' : 'text/plain; charset=utf-8' } ) ; return res . end ( 'This page link has expired. Open the file from Pithos again.' ) ; }
t . exp = Math . min ( now + 60 * 60_000 , t . until ) ;
let segs ;
try { segs = rest . split ( '/' ) . map ( ( x ) => decodeURIComponent ( x ) ) ; } catch { res . writeHead ( 400 ) ; return res . end ( 'bad path' ) ; }
if ( ! segs . length || segs [ segs . length - 1 ] === '' ) segs [ segs . length ? segs . length - 1 : 0 ] = 'index.html' ;
// An encoded slash would make one "segment" span folders (..%2F..).
if ( segs . some ( ( x ) => ! x || x === '.' || x === '..' || /[/\\]/ . test ( x ) ) ) { res . writeHead ( 400 ) ; return res . end ( 'bad path' ) ; }
const key = t . prefix + segs . join ( '/' ) ;
const up = await ( await s3For ( t . user ) ) . getObject ( t . bucket , key , { range : req . headers . range } ) ;
if ( up . statusCode >= 300 && up . statusCode !== 304 ) {
up . resume ? . ( ) ;
res . writeHead ( up . statusCode === 416 ? 416 : 404 , { 'content-type' : 'text/plain; charset=utf-8' , 'content-security-policy' : PAGE _CSP } ) ;
return res . end ( up . statusCode === 416 ? 'range not satisfiable' : 'not found' ) ;
}
// The extension decides for web files: S3 clients often store HTML, CSS or
// scripts as octet-stream (or anything), and a page must load them as such.
const guessed = guessType ( key ) ;
const stored = up . headers [ 'content-type' ] ;
let type = guessed !== 'application/octet-stream' ? guessed : ( stored || guessed ) ;
if ( /^text\/|javascript|json|xml|svg/i . test ( type ) && ! /charset=/i . test ( type ) ) type += '; charset=utf-8' ;
const headers = {
'content-type' : type ,
'content-security-policy' : PAGE _CSP ,
'x-content-type-options' : 'nosniff' ,
'cache-control' : 'no-store' ,
// The sandboxed page is cross-origin even to itself; its scripts may read
// the files its ticket covers (the ticket in the URL is the secret).
'access-control-allow-origin' : '*' ,
} ;
for ( const h of [ 'content-length' , 'content-range' , 'accept-ranges' , 'last-modified' , 'etag' ] ) if ( up . headers [ h ] ) headers [ h ] = up . headers [ h ] ;
res . writeHead ( up . statusCode , headers ) ;
if ( req . method === 'HEAD' ) { up . resume ? . ( ) ; return res . end ( ) ; }
up . on ( 'error' , ( ) => res . destroy ( ) ) ;
up . pipe ( res ) ;
}
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
route ( 'GET' , '/api/s3/:user/buckets/:bucket/text' , async ( req , url , p ) => {
const up = await ( await s3For ( p . user ) ) . getObject ( p . bucket , requireKey ( url ) , { range : 'bytes=0-524287' } ) ;
const body = await readBody ( up ) ;
if ( up . statusCode >= 300 ) throw new HttpError ( up . statusCode , body . match ( /<Message>(.*?)<\/Message>/ ) ? . [ 1 ] || ` HTTP ${ up . statusCode } ` ) ;
const total = Number ( ( up . headers [ 'content-range' ] || '' ) . split ( '/' ) [ 1 ] ) || body . length ;
return { text : body , truncated : total > 524288 , size : total } ;
} ) ;
2026-10-04 00:09:54 +02:00
// Drive usage: files and bytes in a bucket, counted up to 20 pages.
route ( 'GET' , '/api/s3/:user/buckets/:bucket/usage' , async ( req , url , p ) => {
const client = await s3For ( p . user ) ;
let files = 0 , bytes = 0 , token , pages = 0 ;
do {
const page = await client . listObjects ( p . bucket , { delimiter : '' , token } ) ;
for ( const o of page . objects ) { if ( ! o . key . endsWith ( '/' ) ) { files ++ ; bytes += o . size || 0 ; } }
token = page . truncated ? page . nextToken : null ;
} while ( token && ++ pages < 20 ) ;
return { files , bytes , partial : ! ! token } ;
} ) ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
route ( 'POST' , '/api/host/show-panel' , ( ) => {
if ( ! showPanel ) throw new HttpError ( 400 , 'not supported by this host' ) ;
showPanel ( ) ;
return { ok : true } ;
} ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
route ( 'GET' , '/api/s3/:user/buckets/:bucket/presign' , async ( req , url , p ) => {
const q = url . searchParams ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
if ( hosted . isHosted ( ) ) return { url : await hosted . shareLink ( p . user , p . bucket , requireKey ( url ) , Number ( q . get ( 'expires' ) || 3600 ) ) } ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const client = await s3For ( p . user ) ;
return { url : client . presign ( p . bucket , requireKey ( url ) , Number ( q . get ( 'expires' ) || 3600 ) , q . get ( 'endpoint' ) || undefined ) } ;
} ) ;
route ( 'GET' , '/api/s3/:user/buckets/:bucket/policy' , async ( req , url , p ) => {
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
if ( hosted . isHosted ( ) ) return hosted . drivePolicy ( p . bucket ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const policy = await ( await s3For ( p . user ) ) . getPolicy ( p . bucket ) ;
return { policy , public : describePolicy ( policy ) } ;
} ) ;
route ( 'PUT' , '/api/s3/:user/buckets/:bucket/policy' , async ( req , url , p ) => {
const { mode } = await jsonBody ( req ) ; // 'private' | 'read' | 'read-list'
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
if ( hosted . isHosted ( ) ) {
if ( ! [ 'private' , 'read' , 'read-list' ] . includes ( mode ) ) throw new HttpError ( 400 , 'mode must be private, read or read-list' ) ;
await hosted . setDrivePolicy ( p . user , p . bucket , mode ) ;
return { ok : true } ;
}
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const client = await s3For ( p . user ) ;
if ( mode === 'private' ) { await client . deletePolicy ( p . bucket ) . catch ( ( e ) => { if ( e . status !== 404 ) throw e ; } ) ; }
else if ( mode === 'read' || mode === 'read-list' ) await client . putPolicy ( p . bucket , publicReadPolicy ( p . bucket , { list : mode === 'read-list' } ) ) ;
else throw new HttpError ( 400 , 'mode must be private, read or read-list' ) ;
return { ok : true } ;
} ) ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
hosted = installHosted ( {
route , configFile , cfg , daemon , broadcast , registration , HttpError , secrets ,
resetRegistration : ( ) => { regCache = null ; } ,
hasSubscribers : ( ) => subscribers . size > 0 ,
} ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
// ---- server ------------------------------------------------------------
const STREAMED = Symbol ( 'streamed' ) ;
let boundPort = port ;
2026-10-04 03:29:28 +02:00
// Site mode: Theseus relays pithos.sia/<path> here (header x-pithos-site).
// Pithos answers its own files, its pages and /<S3 user>/…; anything else
// is "not mine", so the pithos.sia website answers it instead.
const SITE _PAGES = new Set ( [ 'overview' , 'drives' , 'users' , 'account' , 'start' , 'setup' , 'settings' , 'logs' ] ) ;
const SITE _FILES = new Set ( [ 'app.js' , 'app.css' , 'icon.svg' ] ) ;
let siteUsersCache = { at : 0 , set : new Set ( ) } ;
async function siteUsers ( ) {
if ( Date . now ( ) - siteUsersCache . at > 10_000 ) {
2026-10-04 03:32:04 +02:00
// Hosted drives' users live on the server; the local s3d has none then.
try { siteUsersCache = { at : Date . now ( ) , set : new Set ( hosted ? . isHosted ( ) ? await hosted . userNames ( ) : await cli . listUsers ( ) ) } ; }
2026-10-04 03:29:28 +02:00
catch { siteUsersCache . at = Date . now ( ) ; } // keep the last good list
}
return siteUsersCache . set ;
}
async function serveSite ( req , res , url ) {
const segs = url . pathname . split ( '/' ) . filter ( Boolean ) ;
let first = '' ;
try { first = decodeURIComponent ( segs [ 0 ] || '' ) ; } catch { }
if ( segs . length === 1 && SITE _FILES . has ( first ) ) return serveStatic ( req , res , url ) ;
if ( ! ( SITE _PAGES . has ( first ) || ( await siteUsers ( ) ) . has ( first ) ) ) {
res . writeHead ( 404 , { 'x-pithos-not-mine' : '1' } ) ;
return res . end ( 'not found' ) ;
}
return serveStatic ( req , res , new URL ( '/' , url ) , { base : '/' } ) ;
}
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
function hostAllowed ( h ) {
if ( ! h ) return false ;
const ok = [ ` 127.0.0.1: ${ boundPort } ` , ` localhost: ${ boundPort } ` , ` [::1]: ${ boundPort } ` , ... allowedHosts ] ;
return ok . includes ( h . toLowerCase ( ) ) ;
}
function cookieSession ( req ) {
const m = /(?:^|;\s*)pithos=([^;]+)/ . exec ( req . headers . cookie || '' ) ;
return m && sessions . has ( m [ 1 ] ) ? m [ 1 ] : null ;
}
function issueSession ( res ) {
const id = crypto . randomBytes ( 24 ) . toString ( 'base64url' ) ;
sessions . add ( id ) ;
res . setHeader ( 'set-cookie' , ` pithos= ${ id } ; HttpOnly; SameSite=Strict; Path=/ ` ) ;
return id ;
}
const server = http . createServer ( async ( req , res ) => {
// DNS-rebinding guard: a page on evil.example resolving to 127.0.0.1 still
// sends its own Host header.
if ( ! hostAllowed ( req . headers . host ) ) { res . writeHead ( 421 ) ; return res . end ( 'misdirected request' ) ; }
2026-10-03 19:34:53 +02:00
let url = new URL ( req . url , ` http:// ${ req . headers . host } ` ) ;
let viaTicket = false ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
const dl = /^\/(dl|v)\/([A-Za-z0-9_-]{20,})$/ . exec ( url . pathname ) ;
if ( dl && ( req . method === 'GET' || req . method === 'HEAD' ) ) {
const t = tickets . get ( dl [ 2 ] ) ;
if ( dl [ 1 ] === 'dl' || ( t && ! t . reuse ) ) tickets . delete ( dl [ 2 ] ) ;
2026-10-03 19:34:53 +02:00
if ( ! t || t . exp < Date . now ( ) ) { res . writeHead ( 410 ) ; return res . end ( 'link expired' ) ; }
url = new URL ( t . path , url ) ;
viaTicket = true ;
}
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
res . setHeader ( 'referrer-policy' , 'no-referrer' ) ;
Pithos: open HTML files as a page, or read them as text
An .html file in a drive could only be read as source. It can now also be
opened as a web page in a new tab (Theseus tab, browser tab, or the system
browser from the desktop app), with the CSS, images, scripts and pages
beside it loading as they would from a website.
The page is served under a sandbox CSP without allow-same-origin, so its
scripts run in an opaque origin: they cannot read the Pithos session or
reach its API, only the files of the folder the page link covers. Links
are random, last an hour from last use (12 hours at most), and refuse
paths that leave the folder. Pages decrypt on the fly for drives hosted
on Silent Mode.
2026-10-04 03:49:23 +02:00
const page = /^\/page\/([A-Za-z0-9_-]{20,})\/(.*)$/ . exec ( url . pathname ) ;
if ( page && ( req . method === 'GET' || req . method === 'HEAD' ) ) {
// A page may frame its own folder's files; nothing else frames Pithos.
res . setHeader ( 'x-frame-options' , 'SAMEORIGIN' ) ;
try { return await servePage ( req , res , page [ 1 ] , page [ 2 ] ) ; } catch ( e ) {
if ( res . headersSent ) return res . destroy ( ) ;
res . writeHead ( e instanceof S3Error && e . status < 500 ? e . status : 502 , { 'content-type' : 'text/plain; charset=utf-8' } ) ;
return res . end ( e . message ) ;
}
}
res . setHeader ( 'x-frame-options' , 'DENY' ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
try {
// Session bootstrap: ?t=<secret> on any page swaps the secret for a cookie.
const t = url . searchParams . get ( 't' ) ;
if ( t && safeEqual ( t , secret ) && req . method === 'GET' && ! url . pathname . startsWith ( '/api/' ) ) {
issueSession ( res ) ;
res . writeHead ( 302 , { location : url . pathname } ) ;
return res . end ( ) ;
}
if ( url . pathname === '/api/session' ) {
if ( req . method === 'GET' ) return sendJson ( res , 200 , { authenticated : ! ! cookieSession ( req ) , passwordLogin : ! ! password , host : hostName } ) ;
if ( req . method === 'POST' ) {
requireCsrf ( req ) ;
const body = await jsonBody ( req ) ;
if ( ! password || ! safeEqual ( String ( body . password || '' ) , password ) ) {
await new Promise ( ( r ) => setTimeout ( r , 750 ) ) ;
throw new HttpError ( 401 , 'wrong password' ) ;
}
issueSession ( res ) ;
return sendJson ( res , 200 , { authenticated : true } ) ;
}
if ( req . method === 'DELETE' ) {
const s = cookieSession ( req ) ;
if ( s ) sessions . delete ( s ) ;
res . setHeader ( 'set-cookie' , 'pithos=; Max-Age=0; Path=/' ) ;
return sendJson ( res , 200 , { authenticated : false } ) ;
}
}
if ( url . pathname . startsWith ( '/api/' ) ) {
2026-10-03 19:34:53 +02:00
if ( ! viaTicket && ! cookieSession ( req ) ) throw new HttpError ( 401 , 'not signed in' ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
if ( req . method !== 'GET' ) requireCsrf ( req ) ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
if ( await hosted . intercept ( req , res , url , ( out ) => sendJson ( res , 200 , out ) ) ) return ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
if ( url . pathname === '/api/events' ) {
res . writeHead ( 200 , { 'content-type' : 'text/event-stream' , 'cache-control' : 'no-store' , connection : 'keep-alive' } ) ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
res . write ( ` event: status \n data: ${ JSON . stringify ( hosted . isHosted ( ) ? hosted . remoteDaemon ( ) : daemon . status ( ) ) } \n \n ` ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
res . write ( ` event: login \n data: ${ JSON . stringify ( login . snapshot ( ) ) } \n \n ` ) ;
subscribers . add ( res ) ;
const ping = setInterval ( ( ) => res . write ( ': ping\n\n' ) , 25_000 ) ;
req . on ( 'close' , ( ) => { clearInterval ( ping ) ; subscribers . delete ( res ) ; } ) ;
return ;
}
for ( const r of routes ) {
if ( r . method !== req . method ) continue ;
const m = r . re . exec ( url . pathname ) ;
if ( ! m ) continue ;
const params = Object . fromEntries ( r . keys . map ( ( k , i ) => [ k , decodeURIComponent ( m [ i + 1 ] ) ] ) ) ;
const out = await r . handler ( req , url , params , res ) ;
if ( out === STREAMED ) return ;
return sendJson ( res , 200 , out ? ? { ok : true } ) ;
}
throw new HttpError ( 404 , 'no such endpoint' ) ;
}
2026-10-04 03:29:28 +02:00
if ( req . headers [ 'x-pithos-site' ] === '1' ) return await serveSite ( req , res , url ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
return serveStatic ( req , res , url ) ;
} catch ( err ) {
const status = err instanceof HttpError ? err . status
: err instanceof S3Error ? ( err . status >= 400 && err . status < 600 ? err . status : 502 )
: err instanceof CliError ? 400
: 500 ;
if ( status === 500 ) console . error ( '[pithos]' , err ) ;
if ( res . headersSent ) return res . destroy ( ) ;
sendJson ( res , status , { error : err . message , code : err . code } ) ;
}
} ) ;
await new Promise ( ( resolve , reject ) => {
server . once ( 'error' , reject ) ;
server . listen ( port , host , resolve ) ;
} ) ;
boundPort = server . address ( ) . port ;
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
// Bring s3d back if the user had it running. A crash (not a Stop) is
// retried a few times with a pause, but never a missing Sia connection.
let restarts = [ ] ;
lastAutoTry = Date . now ( ) ;
const wantRunning = ( ) => readPrefs ( configFile ) . autostart === true ;
async function autostart ( reason ) {
if ( daemon . child ) return ;
if ( ! wantRunning ( ) ) return ; // the user stopped it, or never started it
if ( ! daemon . binary ) { daemon . pushLog ( 'sys' , 'not starting s3d: it is not installed' ) ; return ; }
if ( await probeExternal ( cfg ( ) ) ) { daemon . pushLog ( 'sys' , 'not starting s3d: another s3d already serves this config' ) ; return ; }
try { ensureConfig ( configFile ) ; daemon . start ( ) ; daemon . pushLog ( 'sys' , ` started by Pithos ( ${ reason } ) ` ) ; }
catch ( e ) { daemon . pushLog ( 'sys' , ` could not start s3d: ${ e . message } ` ) ; }
}
daemon . on ( 'status' , ( st ) => {
if ( st . state !== 'crashed' || st . needsLogin || ! wantRunning ( ) ) return ;
restarts = restarts . filter ( ( t ) => Date . now ( ) - t < 10 * 60_000 ) ;
if ( restarts . length >= 3 ) { daemon . pushLog ( 'sys' , 's3d keeps stopping; not restarting again for now. See the log above.' ) ; return ; }
restarts . push ( Date . now ( ) ) ;
setTimeout ( ( ) => autostart ( 'it stopped unexpectedly' ) , 5000 ) . unref ? . ( ) ;
} ) ;
setTimeout ( ( ) => autostart ( 'it was running before' ) , 500 ) . unref ? . ( ) ;
2026-10-04 01:04:23 +02:00
const autoFlushTimer = setInterval ( ( ) => { autoFlush . tick ( ) ; } , 30_000 ) ;
autoFlushTimer . unref ? . ( ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
const displayHost = host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host ;
const baseUrl = ` http:// ${ displayHost . includes ( ':' ) ? ` [ ${ displayHost } ] ` : displayHost } : ${ boundPort } / ` ;
return {
server ,
daemon ,
url : baseUrl ,
// Opening this URL signs the view in.
authUrl : ` ${ baseUrl } ?t= ${ secret } ` ,
2026-10-03 19:34:53 +02:00
// For hosts that call the API themselves (the Theseus sidebar bridge):
// a session cookie that never touches a browser.
internalSession ( ) {
const id = crypto . randomBytes ( 24 ) . toString ( 'base64url' ) ;
sessions . add ( id ) ;
return ` pithos= ${ id } ` ;
} ,
// A single-use, 60-second URL for one object download.
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
// A 10-minute link that streams one object inline, for the viewer in
// hosts whose page has no session (the Theseus panel).
viewUrl ( apiPath ) {
if ( ! /^\/api\/s3\/[^/]+\/buckets\/[^/]+\/object\?/ . test ( apiPath ) ) throw new Error ( 'only objects' ) ;
const id = crypto . randomBytes ( 24 ) . toString ( 'base64url' ) ;
tickets . set ( id , { path : apiPath + '&inline=1' , exp : Date . now ( ) + 10 * 60_000 , reuse : true } ) ;
return ` ${ baseUrl } v/ ${ id } ` ;
} ,
2026-10-03 19:34:53 +02:00
downloadUrl ( apiPath ) {
if ( ! /^\/api\/s3\/[^/]+\/buckets\/[^/]+\/object\?/ . test ( apiPath ) ) throw new Error ( 'only object downloads' ) ;
const id = crypto . randomBytes ( 24 ) . toString ( 'base64url' ) ;
tickets . set ( id , { path : apiPath , exp : Date . now ( ) + 60_000 } ) ;
for ( const [ k , v ] of tickets ) if ( v . exp < Date . now ( ) ) tickets . delete ( k ) ;
return ` ${ baseUrl } dl/ ${ id } ` ;
} ,
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
async close ( ) {
2026-10-04 01:04:23 +02:00
clearInterval ( autoFlushTimer ) ;
Pithos: run your drives on Silent Mode with your own Sia account
s3d binds one Sia account per process, so the server runs one s3d per
person (pithos-hosted), started on demand and flushed and stopped when idle.
Only the connected s3d data folder moves between PC and server; the Sia
recovery phrase never leaves the PC and nothing is re-uploaded to Sia.
Because the server holds the connection key, local Pithos encrypts names and
contents before they leave the machine, with keys from the Theseus vault (or
a local key file elsewhere). What s3d cannot do on its own, the gateway now
enforces: public folders, expiring share links and S3 keys for other people
limited to one folder with read, add-only or edit rights. Files those reach
are stored readable, since visitors have no key, and go back to encrypted
when the rule is removed. Sign-up needs a BNS name signature, a deposit or
an invite, with a per-IP limit.
2026-10-04 01:33:20 +02:00
hosted . close ( ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
login . cancel ( ) ;
for ( const s of subscribers ) s . end ( ) ;
await daemon . stop ( ) ;
2026-10-04 20:45:04 +02:00
// Idle keep-alive sockets would hold close() for seconds; nobody is left to answer.
const closed = new Promise ( ( r ) => server . close ( r ) ) ;
server . closeAllConnections ? . ( ) ;
await closed ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
} ,
} ;
}
// ---- helpers -------------------------------------------------------------
function sendJson ( res , status , body ) {
const data = JSON . stringify ( body ) ;
res . writeHead ( status , { 'content-type' : 'application/json; charset=utf-8' , 'cache-control' : 'no-store' } ) ;
res . end ( data ) ;
}
function requireCsrf ( req ) {
if ( req . headers [ 'x-pithos' ] !== '1' ) throw new HttpError ( 403 , 'missing x-pithos header' ) ;
}
function requireKey ( url ) {
const key = url . searchParams . get ( 'key' ) ;
if ( ! key ) throw new HttpError ( 400 , 'key required' ) ;
return key ;
}
async function jsonBody ( req ) {
const chunks = [ ] ;
let size = 0 ;
for await ( const c of req ) {
size += c . length ;
if ( size > 1 << 20 ) throw new HttpError ( 413 , 'body too large' ) ;
chunks . push ( c ) ;
}
if ( ! chunks . length ) return { } ;
try { return JSON . parse ( Buffer . concat ( chunks ) . toString ( 'utf8' ) ) ; } catch { throw new HttpError ( 400 , 'invalid JSON' ) ; }
}
function safeEqual ( a , b ) {
const x = Buffer . from ( String ( a ) ) ;
const y = Buffer . from ( String ( b ) ) ;
return x . length === y . length && crypto . timingSafeEqual ( x , y ) ;
}
function getIn ( obj , keys ) {
return keys . reduce ( ( o , k ) => ( o == null ? undefined : o [ k ] ) , obj ) ;
}
Pithos 0.3.5: s3d stays up, one view at a time, user buckets, a viewer
- s3d kept stopping: Pithos stopped it whenever the extension restarted
(every update, every Theseus quit) and never started it again. Start
and Stop now record what the user wants (pithos-prefs.json beside
s3d.yml); Pithos starts s3d with Theseus and when opened, and retries a
crash up to three times in ten minutes, logging why it skips. The
Buckets page also redraws when s3d starts or stops; it used to keep
saying "not running".
- One view at a time: "Full page" opens Pithos in a tab and closes the
left panel; the tab has "Back to sidebar", which reopens the panel.
- Every new user gets a bucket of their own, named after them (lowercase,
hyphens) or as typed in the New user dialog.
- Buckets show as folder tiles. New folders are kept while empty with the
usual "<name>/" marker object.
- A viewer: images, audio (mp3 and friends), video and text open inside
Pithos, with Share and Download. The Theseus panel uses 10-minute,
reusable, inline view links so players can make range requests.
- The Access dialog says what s3d cannot do: per-folder public access and
write rights for others are not supported, only whole-bucket public read.
2026-10-04 00:05:49 +02:00
// A valid S3 bucket name from a user name: lowercase letters, digits and
// hyphens, 3-63 characters.
function bucketNameFor ( name ) {
let b = String ( name || '' ) . toLowerCase ( ) . replace ( /[^a-z0-9-]+/g , '-' ) . replace ( /-+/g , '-' ) . replace ( /^-+|-+$/g , '' ) ;
if ( b . length < 3 ) b = ( b || 'my' ) + '-files' ;
return b . slice ( 0 , 63 ) . replace ( /-+$/ , '' ) ;
}
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
function cmpVersion ( a , b ) {
const pa = a . replace ( /^v/ , '' ) . split ( /[.-]/ ) . map ( ( n ) => parseInt ( n , 10 ) || 0 ) ;
const pb = b . replace ( /^v/ , '' ) . split ( /[.-]/ ) . map ( ( n ) => parseInt ( n , 10 ) || 0 ) ;
for ( let i = 0 ; i < 3 ; i ++ ) if ( ( pa [ i ] || 0 ) !== ( pb [ i ] || 0 ) ) return ( pa [ i ] || 0 ) - ( pb [ i ] || 0 ) ;
return 0 ;
}
function describePolicy ( policy ) {
if ( ! policy ) return 'private' ;
const actions = new Set ( ) ;
for ( const s of [ ] . concat ( policy . Statement || [ ] ) ) {
if ( s . Effect !== 'Allow' ) continue ;
if ( s . Principal !== '*' && s . Principal ? . AWS !== '*' ) continue ;
for ( const a of [ ] . concat ( s . Action || [ ] ) ) actions . add ( a ) ;
}
if ( actions . has ( 's3:ListBucket' ) ) return 'read-list' ;
if ( actions . has ( 's3:GetObject' ) ) return 'read' ;
return 'custom' ;
}
2026-10-04 03:29:28 +02:00
function serveStatic ( req , res , url , { base } = { } ) {
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
if ( req . method !== 'GET' && req . method !== 'HEAD' ) { res . writeHead ( 405 ) ; return res . end ( ) ; }
let rel = decodeURIComponent ( url . pathname ) ;
if ( rel === '/' || ! path . extname ( rel ) ) rel = '/index.html' ;
const file = path . join ( UI _DIR , path . normalize ( rel ) ) ;
if ( ! file . startsWith ( UI _DIR + path . sep ) ) { res . writeHead ( 403 ) ; return res . end ( ) ; }
fs . readFile ( file , ( err , data ) => {
if ( err ) { res . writeHead ( 404 ) ; return res . end ( 'not found' ) ; }
res . writeHead ( 200 , {
'content-type' : MIME [ path . extname ( file ) ] || 'application/octet-stream' ,
'cache-control' : 'no-cache' ,
'content-security-policy' : "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; connect-src 'self'; frame-ancestors 'none'" ,
} ) ;
2026-10-04 03:29:28 +02:00
// Pages under pithos.sia/<user>/<drive>/… load the app's files from the root.
if ( base && file . endsWith ( 'index.html' ) ) data = Buffer . from ( data . toString ( 'utf8' ) . replace ( '<head>' , ` <head><base href=" ${ base } "> ` ) ) ;
Pithos: a control panel for s3d, the Sia S3 gateway
s3d is driven entirely by a CLI and a two-route admin API, which keeps
Sia storage out of reach for anyone who does not live in a terminal.
Pithos wraps it: install the pinned s3d release (checksum verified),
start/stop and logs, indexer login, users and access keys, a bucket and
object browser with share links and public-read policies, and the
upload pipeline stats.
One control server (core/) serves one UI (ui/), and three hosts run it:
a web console, an Electron desktop window and a Theseus add-on, so a
feature lands everywhere at once. The server binds loopback, rejects
foreign Host headers, and signs views in with a one-time secret that
becomes a SameSite=Strict cookie. s3d login reads the recovery phrase
with term.ReadPassword, so it is driven through a pty, with a console
window fallback where the pty module is not shipped.
2026-10-03 19:02:50 +02:00
res . end ( req . method === 'HEAD' ? undefined : data ) ;
} ) ;
}