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
// pithos-hosted: s3d on Silent Mode with each person's own Sia account.
//
// One HTTP listener (behind nginx/TLS) serves four kinds of traffic:
//
// /hosted/v1/... sign-in, sign-up gate, access rules, moving a data
// folder in and out (Bearer session token)
// /api/... the owner's own Pithos API, forwarded to their
// per-account control server (Bearer session token)
// /p/<id>/<drive>/.. public files, /l/<token> share links (no auth)
// anything else S3, path-style. Owner keys pass straight through to
// their s3d (which checks the signature itself); keys
// minted for other people are verified and limited here,
// then re-signed with the owner's key.
import http from 'node:http' ;
import fs from 'node:fs' ;
import path from 'node:path' ;
import crypto from 'node:crypto' ;
import { pipeline } from 'node:stream/promises' ;
import { readConfig } from '../core/config.js' ;
import { objectPath } from '../core/s3.js' ;
import { createAuth , accountId , challengeText , verifyEd25519 } from './auth.js' ;
import { createGate , bnsMessage , depositMemo } from './gate.js' ;
import { createSupervisor , httpError } from './supervisor.js' ;
import { parseAuth , verify , SigError } from './sigv4.js' ;
import { validateRule , classify , decideGrant , publicRuleFor , live , forOwner } from './rules.js' ;
import { readJson , writeJson } from './store.js' ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
import { isTransferPath , transferTarget , hashFile } from '../core/hosted.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
// Owner API calls the gateway forwards. Everything that would act on the
// server itself (binary, s3d.yml, Sia login, account switching) is the
// gateway's business, and object routes are refused so nothing can bypass
// the client-side encryption.
const FORWARD = [
[ 'GET' , /^\/api\/status$/ ] , [ 'GET' , /^\/api\/logs$/ ] , [ 'GET' , /^\/api\/stats$/ ] ,
[ 'POST' , /^\/api\/flush$/ ] , [ 'GET' , /^\/api\/autoflush$/ ] ,
[ 'POST' , /^\/api\/daemon\/(start|restart)$/ ] ,
[ 'GET' , /^\/api\/users$/ ] , [ 'POST' , /^\/api\/users$/ ] , [ 'DELETE' , /^\/api\/users\/[^/]+$/ ] ,
[ 'GET' , /^\/api\/keys$/ ] , [ 'POST' , /^\/api\/keys$/ ] , [ 'DELETE' , /^\/api\/keys\/[^/]+$/ ] ,
[ 'GET' , /^\/api\/account$/ ] , [ 'PUT' , /^\/api\/account\/label$/ ] ,
2026-10-04 04:01:36 +02:00
[ 'GET' , /^\/api\/space$/ ] ,
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 KEY _CHANGES = /^\/api\/(users|keys)/ ;
export async function createHosted ( cfg , log = console . log ) {
fs . mkdirSync ( cfg . root , { recursive : true } ) ;
const auth = createAuth ( cfg . root ) ;
const grantsFile = path . join ( cfg . root , 'grants.json' ) ;
const linksFile = path . join ( cfg . root , 'links.json' ) ;
const grants = readJson ( grantsFile , { } ) ; // accessKeyId -> { id, rule }
const links = readJson ( linksFile , { } ) ; // token -> { id, rule }
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
const gate = createGate ( cfg . root , cfg . gate || { } ) ;
// Grant key ids are the gateway's: no account's s3d may route the same id.
const sup = createSupervisor ( cfg , log , { reservedKey : ( akid ) => / ^ PG / . test ( akid ) || Object . hasOwn ( grants , akid ) } ) ;
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 rulesFile = ( id ) => path . join ( sup . dir ( id ) , 'rules.json' ) ;
const rulesOf = ( id ) => readJson ( rulesFile ( id ) , [ ] ) ;
function saveRules ( id , rules ) {
writeJson ( rulesFile ( id ) , rules ) ;
for ( const [ k , v ] of Object . entries ( grants ) ) if ( v . id === id ) delete grants [ k ] ;
for ( const [ k , v ] of Object . entries ( links ) ) if ( v . id === id ) delete links [ k ] ;
for ( const r of rules ) {
if ( r . type === 'grant' ) grants [ r . accessKeyId ] = { id , rule : r . id } ;
if ( r . type === 'link' ) links [ r . token ] = { id , rule : r . id } ;
}
writeJson ( grantsFile , grants ) ;
writeJson ( linksFile , links ) ;
}
const ruleById = ( id , ruleId ) => rulesOf ( id ) . find ( ( r ) => r . id === ruleId ) || null ;
// ---- traffic meter -------------------------------------------------------
const month = ( ) => new Date ( ) . toISOString ( ) . slice ( 0 , 7 ) ;
const usage = new Map ( ) ; // id -> { month, down, up }
function usageOf ( id ) {
let u = usage . get ( id ) ;
if ( ! u ) { u = readJson ( path . join ( sup . dir ( id ) , 'usage.json' ) , null ) || { month : month ( ) , down : 0 , up : 0 } ; usage . set ( id , u ) ; }
if ( u . month !== month ( ) ) { u . month = month ( ) ; u . down = 0 ; u . up = 0 ; }
return u ;
}
const meterTimer = setInterval ( ( ) => {
for ( const [ id , u ] of usage ) writeJson ( path . join ( sup . dir ( id ) , 'usage.json' ) , u ) ;
} , 30_000 ) ;
meterTimer . unref ? . ( ) ;
function checkCap ( id ) {
if ( cfg . downloadCapBytes && usageOf ( id ) . down >= cfg . downloadCapBytes ) {
throw httpError ( 429 , ` this account has used its ${ fmtBytes ( cfg . downloadCapBytes ) } of downloads this month ` ) ;
}
}
// ---- helpers ---------------------------------------------------------------
const clientIp = ( req ) => ( cfg . trustProxy && String ( req . headers [ 'x-forwarded-for' ] || '' ) . split ( ',' ) [ 0 ] . trim ( ) ) || req . socket . remoteAddress || '' ;
function bearer ( req ) {
const m = /^Bearer\s+(\S+)$/ . exec ( req . headers . authorization || '' ) ;
const id = m && auth . lookup ( m [ 1 ] ) ;
if ( ! id ) throw httpError ( 401 , 'sign in first' ) ;
return { id , token : m [ 1 ] } ;
}
async function forwardApi ( req , res , url , id ) {
const a = sup . account ( id ) ;
if ( url . pathname === '/api/status' && a . state !== 'active' ) {
return json ( res , 200 , hostedStatus ( a , { daemon : { state : 'stopped' } , registration : { registered : false } } ) ) ;
}
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
const reqBody = req . method === 'GET' ? null : JSON . stringify ( await jsonBody ( req ) ) ;
// A chosen access key id must not be one another account (or a grant)
// routes: S3 traffic is sent to whichever s3d owns the id.
if ( req . method === 'POST' && url . pathname === '/api/keys' ) {
const want = JSON . parse ( reqBody ) . accessKey ;
if ( want && sup . keyTaken ( String ( want ) , id ) ) throw httpError ( 409 , 'that access key id is already in use; leave it empty to get a random one' ) ;
}
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 inst = await sup . ensure ( id ) ;
const release = sup . hold ( inst ) ;
try {
const target = new URL ( url . pathname + url . search , inst . pithos . url ) ;
const up = await new Promise ( ( resolve , reject ) => {
const r = http . request ( target , {
method : req . method ,
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
headers : { cookie : inst . cookie , 'x-pithos' : '1' , 'content-type' : 'application/json' } ,
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
} , resolve ) ;
r . on ( 'error' , reject ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
r . end ( reqBody ? ? undefined ) ;
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 text = await readAll ( up ) ;
if ( req . method !== 'GET' && KEY _CHANGES . test ( url . pathname ) ) await sup . indexKeys ( inst ) ;
let body = text ;
if ( up . statusCode === 200 && url . pathname === '/api/status' ) body = JSON . stringify ( hostedStatus ( a , JSON . parse ( text ) ) ) ;
if ( up . statusCode === 200 && url . pathname === '/api/account' ) {
const acc = JSON . parse ( text ) ;
2026-10-04 03:19:02 +02:00
body = JSON . stringify ( { indexerUrl : acc . indexerUrl , registered : acc . registered , label : acc . label , sia : acc . sia || null , archives : [ ] , hosted : 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
}
res . writeHead ( up . statusCode , { 'content-type' : 'application/json; charset=utf-8' , 'cache-control' : 'no-store' } ) ;
res . end ( body ) ;
} finally { release ( ) ; }
}
// Server paths stay on the server.
function hostedStatus ( a , st ) {
return {
... st ,
host : 'hosted' ,
config : st . config ? { apiAddress : null , adminAddress : null , file : null , directory : null , https : null , exists : true } : undefined ,
2026-10-04 16:10:11 +02:00
hosted : { id : a . id , name : a . gate ? . type === 'bns' ? a . gate . name : null , state : a . state , publicUrl : cfg . publicUrl , usage : usageOf ( a . id ) , downloadCapBytes : cfg . downloadCapBytes || 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
} ;
}
// ---- data folder in and out ------------------------------------------------
const incomingDir = ( id ) => path . join ( sup . dir ( id ) , 'incoming' ) ;
const incomingManifest = ( id ) => path . join ( sup . dir ( id ) , 'incoming.json' ) ;
function safeRel ( rel ) {
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
if ( ! isTransferPath ( rel ) ) throw httpError ( 400 , 'bad file path' ) ;
return rel ;
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
}
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
// Bytes actually on disk under a folder: aborted uploads count too.
function diskBytes ( dir ) {
let n = 0 ;
const rec = ( d ) => {
let entries = [ ] ;
try { entries = fs . readdirSync ( d , { withFileTypes : true } ) ; } catch { return ; }
for ( const e of entries ) {
const p = path . join ( d , e . name ) ;
if ( e . isDirectory ( ) ) rec ( p ) ;
else if ( e . isFile ( ) ) { try { n += fs . statSync ( p ) . size ; } catch { } }
}
} ;
rec ( dir ) ;
return n ;
}
// One upload at a time per account, so the size limit cannot be raced.
const receiving = new Set ( ) ;
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
async function receiveFile ( req , id , rel ) {
const a = sup . account ( id ) ;
if ( a . state !== 'empty' ) throw httpError ( 409 , 'drives are already hosted here; move them back to your computer first' ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
const name = safeRel ( rel ) ;
if ( receiving . has ( id ) ) throw httpError ( 409 , 'send one file at a time' ) ;
receiving . add ( id ) ;
const file = transferTarget ( incomingDir ( id ) , name ) ;
try {
fs . mkdirSync ( path . dirname ( file ) , { recursive : true } ) ;
fs . rmSync ( file , { force : true } ) ;
const limit = cfg . maxDataBytes || 4 * 1024 * * 3 ;
const room = limit - diskBytes ( incomingDir ( id ) ) ;
const declared = Number ( req . headers [ 'content-length' ] ) ;
if ( Number . isFinite ( declared ) && declared > room ) throw httpError ( 413 , 'the data folder is larger than this server accepts' ) ;
const hash = crypto . createHash ( 'sha256' ) ;
let size = 0 ;
req . on ( 'data' , ( c ) => { hash . update ( c ) ; size += c . length ; if ( size > room ) req . destroy ( httpError ( 413 , 'the data folder is larger than this server accepts' ) ) ; } ) ;
await pipeline ( req , fs . createWriteStream ( file ) ) ;
const man = readJson ( incomingManifest ( id ) , { } ) ;
man [ name ] = { size , sha256 : hash . digest ( 'hex' ) } ;
writeJson ( incomingManifest ( id ) , man ) ;
return man [ name ] ;
} catch ( e ) {
fs . rmSync ( file , { force : true } ) ;
throw e ;
} finally {
receiving . delete ( id ) ;
}
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
}
async function commit ( id , files ) {
const a = sup . account ( id ) ;
if ( a . state !== 'empty' ) throw httpError ( 409 , 'drives are already hosted here' ) ;
const man = readJson ( incomingManifest ( id ) , { } ) ;
if ( ! Array . isArray ( files ) || ! files . length ) throw httpError ( 400 , 'no files listed' ) ;
for ( const f of files ) {
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
const got = Object . hasOwn ( man , safeRel ( f . path ) ) ? man [ f . path ] : 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
if ( ! got || got . sha256 !== f . sha256 || got . size !== f . size ) throw httpError ( 400 , ` ${ f . path } did not arrive intact; send it again ` ) ;
}
if ( Object . keys ( man ) . length !== files . length ) throw httpError ( 400 , 'the server has files the list does not mention; start the move again' ) ;
if ( ! fs . existsSync ( path . join ( incomingDir ( id ) , 's3d.db' ) ) ) throw httpError ( 400 , 'this is not an s3d data folder (no s3d.db)' ) ;
await sup . close ( id , 'new data folder' ) ;
fs . rmSync ( sup . dataDir ( id ) , { recursive : true , force : true } ) ;
fs . renameSync ( incomingDir ( id ) , sup . dataDir ( id ) ) ;
fs . rmSync ( incomingManifest ( id ) , { force : true } ) ;
sup . setState ( id , 'active' ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
// Anything wrong with the folder puts the account back to empty, so the
// owner's computer (which asks the server's state) keeps its copy.
const refuse = async ( status , message ) => {
await sup . close ( id , 'refused data folder' ) ;
fs . rmSync ( sup . dataDir ( id ) , { recursive : true , force : true } ) ;
fs . mkdirSync ( sup . dataDir ( id ) , { recursive : 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
sup . setState ( id , 'empty' ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
throw httpError ( status , message ) ;
} ;
let inst ;
try { inst = await sup . ensure ( id ) ; } catch ( e ) { return refuse ( 502 , ` s3d could not start on the moved folder: ${ e . message } ` ) ; }
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 reg = await inst . cli . registration ( ) . catch ( ( ) => ( { registered : false } ) ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
if ( ! reg . registered ) return refuse ( 422 , 'the moved folder has no Sia connection' ) ;
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 keys = await sup . indexKeys ( inst ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
if ( inst . conflicts ? . length ) return refuse ( 409 , ` these access keys are already used on this server: ${ inst . conflicts . join ( ', ' ) } ; create new keys on your computer and try again ` ) ;
return { registered : true , indexerUrl : reg . indexerUrl || null , users : [ ... new Set ( keys . map ( ( k ) => k . 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
}
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
// Hashes stream from disk (buffered objects can be gigabytes) and only
// paths the owner's Pithos will accept are listed.
async function walk ( base ) {
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 out = [ ] ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
const rec = async ( d ) => {
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
for ( const e of fs . readdirSync ( d , { withFileTypes : true } ) ) {
const p = path . join ( d , e . name ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
const rel = path . relative ( base , p ) . split ( path . sep ) . join ( '/' ) ;
if ( e . isDirectory ( ) ) await rec ( p ) ;
else if ( e . isFile ( ) && isTransferPath ( rel ) ) out . push ( { path : rel , ... ( await hashFile ( 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
}
} ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
await rec ( base ) ;
return out ;
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
}
async function checkout ( id , force ) {
const a = sup . account ( id ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
if ( a . state === 'moving-out' ) return { files : await walk ( sup . dataDir ( id ) ) } ;
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 ( a . state !== 'active' ) throw httpError ( 409 , 'nothing is hosted here' ) ;
const inst = await sup . ensure ( id ) ;
const flushed = await sup . flushQuietly ( inst , 10 * 60_000 ) ;
if ( ! flushed && ! force ) throw httpError ( 409 , 'some uploads could not be sent to Sia yet; try again later, or move anyway and they will finish on your computer' ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
// State first: from now on no request can start s3d on this folder again.
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
sup . setState ( id , 'moving-out' ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
await sup . close ( id , 'moving to the owner\'s computer' ) ;
return { files : await walk ( sup . dataDir ( id ) ) } ;
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
}
function release ( id ) {
const a = sup . account ( id ) ;
if ( a . state !== 'moving-out' ) throw httpError ( 409 , 'start the move first' ) ;
fs . rmSync ( sup . dataDir ( id ) , { recursive : true , force : true } ) ;
fs . mkdirSync ( sup . dataDir ( id ) , { recursive : true } ) ;
saveRules ( id , [ ] ) ;
sup . setState ( id , 'empty' ) ;
return { ok : true } ;
}
// ---- S3 ----------------------------------------------------------------------
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
// For shared keys: "." and ".." segments are refused outright, so a key can
// never be made to look inside its folder while naming something outside.
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
function splitS3Path ( rawPath ) {
const parts = rawPath . split ( '/' ) . slice ( 1 ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
const dec = ( s ) => { try { return decodeURIComponent ( s ) ; } catch { throw httpError ( 400 , 'malformed path' ) ; } } ;
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 bucket = dec ( parts [ 0 ] || '' ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
const keyParts = parts . slice ( 1 ) . map ( dec ) ;
const key = parts . length > 1 ? keyParts . join ( '/' ) : null ;
if ( key != null && key . split ( '/' ) . some ( ( x ) => x === '.' || x === '..' ) ) throw httpError ( 400 , 'paths with . or .. segments are not allowed' ) ;
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 { bucket , key } ;
}
async function s3 ( req , res , url ) {
const a = parseAuth ( req , url ) ;
if ( ! a ) return s3Error ( res , 403 , 'AccessDenied' , 'sign requests with a key; public files are at /p/<account>/<drive>/<path>' ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
const g = Object . hasOwn ( grants , a . akid ) ? grants [ a . akid ] : null ;
const ownerId = g ? null : sup . ownerOfKey ( a . akid ) ;
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 ( ownerId ) {
checkReadCap ( req , ownerId ) ;
const inst = await sup . ensure ( ownerId ) ;
return rawProxy ( req , res , inst ) ;
}
const rule = g && ruleById ( g . id , g . rule ) ;
if ( ! rule ) return s3Error ( res , 403 , 'InvalidAccessKeyId' , 'The AWS Access Key Id you provided does not exist in our records.' ) ;
verify ( req , url , a , rule . secretKey ) ;
const raw = req . url . split ( '?' ) [ 0 ] ;
const { bucket , key } = splitS3Path ( raw ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
// The client's own signature parameters (presigned URLs) must not reach
// s3d next to the owner signature the gateway adds.
const query = Object . fromEntries ( [ ... url . searchParams ] . filter ( ( [ k ] ) => ! /^x-amz-/i . test ( k ) ) ) ;
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 { op } = classify ( req . method , query , key , req . headers ) ;
const d = decideGrant ( rule , bucket , op , key , query . prefix || '' ) ;
if ( ! d . allow ) return s3Error ( res , 403 , 'AccessDenied' , d . reason ) ;
const payload = req . headers [ 'x-amz-content-sha256' ] || 'UNSIGNED-PAYLOAD' ;
if ( payload === 'STREAMING-AWS4-HMAC-SHA256-PAYLOAD' ) {
return s3Error ( res , 501 , 'NotImplemented' , 'signed streaming uploads are not supported with shared keys; turn off payload signing in your S3 client' ) ;
}
checkReadCap ( req , g . id ) ;
const inst = await sup . ensure ( g . id ) ;
const client = await sup . bucketClient ( inst , bucket ) ;
if ( ! client ) return s3Error ( res , 404 , 'NoSuchBucket' , 'The specified bucket does not exist' ) ;
if ( d . needsAbsent ) {
const head = await client . request ( 'HEAD' , objectPath ( bucket , key ) ) ;
head . resume ( ) ;
if ( head . statusCode === 200 ) return s3Error ( res , 403 , 'AccessDenied' , 'this key may add files but not replace them' ) ;
}
const pass = { } ;
for ( const [ k , v ] of Object . entries ( req . headers ) ) {
if ( /^(content-type|content-length|content-md5|content-encoding|content-disposition|cache-control|range|if-(match|none-match|modified-since|unmodified-since))$/ . test ( k ) || ( k . startsWith ( 'x-amz-' ) && ! /^x-amz-(date|content-sha256|security-token)$/ . test ( k ) ) ) pass [ k ] = v ;
}
const release = sup . hold ( inst ) ;
try {
const up = await client . request ( req . method , key == null ? objectPath ( bucket ) : objectPath ( bucket , key ) , { query , headers : pass , body : req , payloadHash : payload } ) ;
relay ( up , res , g . id , req . method ) ;
await done ( res ) ;
} finally { release ( ) ; }
}
function checkReadCap ( req , id ) {
if ( req . method === 'GET' ) checkCap ( id ) ;
}
async function rawProxy ( req , res , inst ) {
const c = readConfig ( sup . configFile ( inst . id ) ) ;
const [ host , port ] = c . apiAddress . split ( ':' ) ;
const release = sup . hold ( inst ) ;
const u = usageOf ( inst . id ) ;
try {
const up = await new Promise ( ( resolve , reject ) => {
const headers = { ... req . headers } ;
delete headers [ 'x-forwarded-for' ] ; delete headers [ 'x-real-ip' ] ; delete headers . connection ;
const r = http . request ( { host , port : Number ( port ) , method : req . method , path : req . url , headers } , resolve ) ;
r . on ( 'error' , reject ) ;
req . on ( 'data' , ( chunk ) => { u . up += chunk . length ; } ) ;
req . pipe ( r ) ;
} ) ;
relay ( up , res , inst . id , req . method ) ;
await done ( res ) ;
} finally { release ( ) ; }
}
function relay ( up , res , id , method ) {
const u = usageOf ( id ) ;
res . writeHead ( up . statusCode , up . headers ) ;
if ( method === 'GET' ) up . on ( 'data' , ( c ) => { u . down += c . length ; } ) ;
up . pipe ( res ) ;
}
// ---- public files and links ----------------------------------------------------
async function serveObject ( req , res , id , bucket , key , { inline = true } = { } ) {
checkCap ( id ) ;
const inst = await sup . ensure ( id ) ;
const client = await sup . bucketClient ( inst , bucket ) ;
if ( ! client ) throw httpError ( 404 , 'not found' ) ;
const release = sup . hold ( inst ) ;
try {
const up = await client . getObject ( bucket , key , { range : req . headers . range } ) ;
if ( up . statusCode >= 300 && up . statusCode !== 304 ) { up . resume ( ) ; throw httpError ( up . statusCode === 403 ? 404 : up . statusCode , 'not found' ) ; }
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' ] = ` ${ inline ? 'inline' : 'attachment' } ; filename*=UTF-8'' ${ encodeURIComponent ( name ) } ` ;
headers [ 'x-content-type-options' ] = 'nosniff' ;
headers [ 'content-security-policy' ] = "sandbox; default-src 'none'; img-src 'self'; media-src 'self'; style-src 'unsafe-inline'" ;
headers [ 'cache-control' ] = 'public, max-age=60' ;
const u = usageOf ( id ) ;
res . writeHead ( up . statusCode , headers ) ;
if ( req . method === 'HEAD' ) { up . resume ( ) ; return res . end ( ) ; }
up . on ( 'data' , ( c ) => { u . down += c . length ; } ) ;
up . pipe ( res ) ;
await done ( res ) ;
} finally { release ( ) ; }
}
2026-10-04 16:10:11 +02:00
// /p/<account id or BNS name>/<drive>/<path>. Also reached as
// navigate.st/s3/…, so every link and redirect here is relative.
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
async function servePublic ( req , res , url ) {
2026-10-04 16:10:11 +02:00
const m = /^\/p\/([^/]+)\/([^/]+)(\/?)(.*)$/ . exec ( url . pathname ) ;
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 ( ! m ) throw httpError ( 404 , 'not found' ) ;
2026-10-04 16:10:11 +02:00
const [ , who , bucket , slash ] = m ;
let whoName = who ;
try { whoName = decodeURIComponent ( who ) ; } catch { }
const id = /^[0-9a-f]{32}$/ . test ( who ) ? who : sup . accountByName ( whoName ) ;
if ( ! id ) throw httpError ( 404 , 'not found' ) ;
// The drive itself without its slash: add it, so relative links resolve.
if ( ! slash && ! m [ 4 ] ) { res . writeHead ( 301 , { location : ` ${ encodeURIComponent ( bucket ) } / ${ url . search } ` } ) ; return res . end ( ) ; }
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
let key ;
try { key = decodeURIComponent ( m [ 3 ] ) ; } catch { throw httpError ( 404 , 'not found' ) ; }
if ( key . split ( '/' ) . some ( ( x ) => x === '.' || x === '..' ) ) throw httpError ( 404 , 'not found' ) ;
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 rule = publicRuleFor ( rulesOf ( id ) , bucket , key ) ;
if ( ! rule ) throw httpError ( 404 , 'not found' ) ;
if ( key === '' || key . endsWith ( '/' ) ) {
if ( rule . rights !== 'read-list' ) throw httpError ( 404 , 'not found' ) ;
const inst = await sup . ensure ( id ) ;
const client = await sup . bucketClient ( inst , bucket ) ;
if ( ! client ) throw httpError ( 404 , 'not found' ) ;
const page = await client . listObjects ( bucket , { prefix : key , token : url . searchParams . get ( 'token' ) || undefined } ) ;
2026-10-04 16:10:11 +02:00
const listing = {
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
folders : page . folders . map ( ( f ) => f . slice ( key . length ) ) ,
files : page . objects . filter ( ( o ) => o . key !== key ) . map ( ( o ) => ( { name : o . key . slice ( key . length ) , size : o . size , modified : o . modified } ) ) ,
next : page . truncated ? page . nextToken : null ,
2026-10-04 16:10:11 +02:00
} ;
// Browsers get a page with links and a player; tools get the JSON.
if ( /text\/html/ . test ( req . headers . accept || '' ) && ! url . searchParams . has ( 'json' ) ) {
res . writeHead ( 200 , { 'content-type' : 'text/html; charset=utf-8' , 'cache-control' : 'no-store' , 'content-security-policy' : "default-src 'self'; style-src 'unsafe-inline'; img-src 'self'; media-src 'self'; script-src 'unsafe-inline'; frame-ancestors 'none'" , 'x-content-type-options' : 'nosniff' } ) ;
return res . end ( folderPage ( { owner : whoName , bucket , key , listing , token : url . searchParams . get ( 'token' ) } ) ) ;
}
return json ( res , 200 , listing ) ;
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 serveObject ( req , res , id , bucket , key ) ;
}
async function serveLink ( req , res , url ) {
const token = url . pathname . slice ( 3 ) ;
Pithos hosting: fixes from an independent review
A server-sent file list could name paths outside the s3d folder (or the key
file in it), and a failed move back deleted every listed path. Transfers now
accept only s3d data paths, check all of them before writing, never
overwrite, and clean up only what they wrote.
A lost reply could lose or fork the drives: move-up and move-down now ask the
server who holds them before touching the local copy, and once the server is
told to release, the local copy is never removed (the release is retried).
The gateway no longer lets one account route another's access key id, refuses
. and .. in shared-key paths, strips presign parameters before re-signing,
counts upload size on disk, stops s3d before it can be restarted mid-move, and
claims sign-up proofs before checking them. The key file is never replaced
when unreadable, a local edit removes the older encrypted copy, listing pages
no longer repeat, and data hashes stream instead of loading whole files.
2026-10-04 03:15:42 +02:00
const l = Object . hasOwn ( links , token ) ? links [ token ] : 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
const rule = l && ruleById ( l . id , l . rule ) ;
if ( ! rule || ! live ( rule ) ) throw httpError ( 410 , 'this link has expired' ) ;
return serveObject ( req , res , l . id , rule . bucket , rule . prefix , { inline : ! url . searchParams . has ( 'download' ) } ) ;
}
// ---- routing -------------------------------------------------------------------
async function handle ( req , res ) {
const url = new URL ( req . url , cfg . publicUrl ) ;
const isS3 = /^AWS4-HMAC-SHA256 / . test ( req . headers . authorization || '' ) || url . searchParams . has ( 'X-Amz-Algorithm' ) ;
if ( isS3 ) return s3 ( req , res , url ) ;
const p = url . pathname ;
if ( p . startsWith ( '/p/' ) && ( req . method === 'GET' || req . method === 'HEAD' ) ) return servePublic ( req , res , url ) ;
if ( p . startsWith ( '/l/' ) && ( req . method === 'GET' || req . method === 'HEAD' ) ) return serveLink ( req , res , url ) ;
if ( p === '/hosted/v1/info' && req . method === 'GET' ) {
return json ( res , 200 , { publicUrl : cfg . publicUrl , gate : gate . describe ( ) , downloadCapBytes : cfg . downloadCapBytes || null , s3Endpoint : cfg . publicUrl } ) ;
}
if ( p === '/hosted/v1/challenge' && req . method === 'GET' ) {
const nonce = auth . challenge ( ) ;
return json ( res , 200 , { nonce , message : challengeText ( cfg . publicUrl , nonce ) } ) ;
}
if ( p === '/hosted/v1/session' && req . method === 'POST' ) {
const body = await jsonBody ( req ) ;
if ( ! auth . takeNonce ( body . nonce ) ) throw httpError ( 401 , 'the sign-in request expired; try again' ) ;
if ( ! verifyEd25519 ( body . pubkey , challengeText ( cfg . publicUrl , body . nonce ) , body . signature ) ) throw httpError ( 401 , 'the signature does not match' ) ;
const id = accountId ( body . pubkey ) ;
let a = sup . account ( id ) ;
if ( ! a ) {
if ( ! body . signup ) {
return json ( res , 404 , { error : 'no hosted account for this key yet' , needsSignup : true , id , gate : gate . describe ( ) , bnsMessage : bnsMessage ( id ) , depositMemo : depositMemo ( id ) } ) ;
}
const g = await gate . admit ( body . signup , id , clientIp ( req ) ) ;
a = sup . createAccount ( { id , pubkey : body . pubkey . toLowerCase ( ) , gate : g } ) ;
log ( ` [ ${ id . slice ( 0 , 8 ) } ] new account via ${ g . type } ` ) ;
}
return json ( res , 200 , { token : auth . issue ( id ) , id , state : a . state } ) ;
}
if ( p . startsWith ( '/hosted/v1/' ) || p . startsWith ( '/api/' ) ) {
const { id , token } = bearer ( req ) ;
if ( p . startsWith ( '/api/' ) ) {
if ( ! FORWARD . some ( ( [ m , re ] ) => m === req . method && re . test ( p ) ) ) throw httpError ( 403 , 'not available on a hosted account' ) ;
return forwardApi ( req , res , url , id ) ;
}
const a = sup . account ( id ) ;
const q = url . searchParams ;
switch ( ` ${ req . method } ${ p } ` ) {
case 'GET /hosted/v1/me' :
return json ( res , 200 , { id , state : a . state , gate : { type : a . gate ? . type , name : a . gate ? . name || null } , createdAt : a . createdAt , usage : usageOf ( id ) , downloadCapBytes : cfg . downloadCapBytes || null , publicUrl : cfg . publicUrl } ) ;
case 'DELETE /hosted/v1/session' :
auth . revoke ( token ) ;
return json ( res , 200 , { ok : true } ) ;
case 'GET /hosted/v1/rules' :
return json ( res , 200 , forOwner ( rulesOf ( id ) ) ) ;
case 'POST /hosted/v1/rules' : {
const inst = await sup . ensure ( id ) ;
const rule = validateRule ( await jsonBody ( req ) , await sup . mapBuckets ( inst ) ) ;
const rules = rulesOf ( id ) . filter ( ( r ) => live ( r ) ) ;
if ( rules . length >= 500 ) throw httpError ( 400 , 'too many rules on one account' ) ;
rules . push ( rule ) ;
saveRules ( id , rules ) ;
return json ( res , 200 , rule ) ;
}
case 'DELETE /hosted/v1/rules' : {
const rules = rulesOf ( id ) ;
const next = rules . filter ( ( r ) => r . id !== q . get ( 'id' ) ) ;
if ( next . length === rules . length ) throw httpError ( 404 , 'no such rule' ) ;
saveRules ( id , next ) ;
return json ( res , 200 , { ok : true } ) ;
}
case 'PUT /hosted/v1/data/file' :
return json ( res , 200 , await receiveFile ( req , id , q . get ( 'path' ) ) ) ;
case 'POST /hosted/v1/data/commit' :
return json ( res , 200 , await commit ( id , ( await jsonBody ( req ) ) . files ) ) ;
case 'POST /hosted/v1/data/abort' :
if ( a . state !== 'empty' ) throw httpError ( 409 , 'nothing to abort' ) ;
fs . rmSync ( incomingDir ( id ) , { recursive : true , force : true } ) ;
fs . rmSync ( incomingManifest ( id ) , { force : true } ) ;
return json ( res , 200 , { ok : true } ) ;
case 'POST /hosted/v1/data/checkout' :
return json ( res , 200 , await checkout ( id , q . get ( 'force' ) === '1' ) ) ;
case 'GET /hosted/v1/data/file' : {
if ( a . state !== 'moving-out' ) throw httpError ( 409 , 'start the move first' ) ;
const file = path . join ( sup . dataDir ( id ) , safeRel ( q . get ( 'path' ) ) ) ;
if ( ! fs . existsSync ( file ) ) throw httpError ( 404 , 'no such file' ) ;
res . writeHead ( 200 , { 'content-type' : 'application/octet-stream' , 'content-length' : fs . statSync ( file ) . size } ) ;
await pipeline ( fs . createReadStream ( file ) , res ) ;
return ;
}
case 'POST /hosted/v1/data/cancel-checkout' :
if ( a . state !== 'moving-out' ) throw httpError ( 409 , 'no move in progress' ) ;
sup . setState ( id , 'active' ) ;
return json ( res , 200 , { ok : true } ) ;
case 'POST /hosted/v1/data/release' :
return json ( res , 200 , release ( id ) ) ;
default :
throw httpError ( 404 , 'no such endpoint' ) ;
}
}
throw httpError ( 404 , 'not found' ) ;
}
const server = http . createServer ( ( req , res ) => {
handle ( req , res ) . catch ( ( err ) => {
const status = err instanceof SigError ? err . status : err . status || 500 ;
if ( status >= 500 ) log ( '[hosted]' , err . stack || err . message ) ;
if ( res . headersSent ) return res . destroy ( ) ;
const isS3 = /^AWS4-HMAC-SHA256 / . test ( req . headers . authorization || '' ) ;
if ( isS3 ) return s3Error ( res , status , err . code || ( status === 404 ? 'NoSuchKey' : 'InternalError' ) , err . message ) ;
json ( res , status , { error : status >= 500 && ! err . status ? 'internal error' : err . message } ) ;
} ) ;
} ) ;
server . requestTimeout = 0 ; // large uploads and downloads stream for a long time
const [ host , port ] = cfg . listen . split ( ':' ) ;
await new Promise ( ( resolve , reject ) => { server . once ( 'error' , reject ) ; server . listen ( Number ( port ) , host , resolve ) ; } ) ;
log ( ` pithos-hosted on ${ cfg . listen } for ${ cfg . publicUrl } ` ) ;
return {
server ,
supervisor : sup ,
async close ( ) {
clearInterval ( meterTimer ) ;
for ( const [ id , u ] of usage ) writeJson ( path . join ( sup . dir ( id ) , 'usage.json' ) , u ) ;
await new Promise ( ( r ) => server . close ( r ) ) ;
await sup . closeAll ( ) ;
} ,
} ;
}
2026-10-04 16:10:11 +02:00
// A public folder as a page: breadcrumbs, folders, files (links are relative
// to the folder, so the page works under /p/ and under navigate.st/s3/),
// pictures shown small, music and video playable in place.
const PAGE _ESC = ( s ) => String ( s ) . replace ( /[&<>"']/g , ( c ) => ( { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' } [ c ] ) ) ;
const fmtSize = ( n ) => { const u = [ 'B' , 'KiB' , 'MiB' , 'GiB' , 'TiB' ] ; let i = 0 ; n = Number ( n ) || 0 ; while ( n >= 1024 && i < u . length - 1 ) { n /= 1024 ; i ++ ; } return ` ${ i ? n . toFixed ( 1 ) : n } ${ u [ i ] } ` ; } ;
export function folderPage ( { owner , bucket , key , listing , token } ) {
const parts = key . split ( '/' ) . filter ( Boolean ) ;
const up = ( n ) => '../' . repeat ( n ) || './' ;
const crumbs = [ ` <a href=" ${ up ( parts . length ) } "> ${ PAGE _ESC ( bucket ) } </a> ` , ... parts . map ( ( p , i ) => i === parts . length - 1 ? ` <span> ${ PAGE _ESC ( p ) } </span> ` : ` <a href=" ${ up ( parts . length - 1 - i ) } "> ${ PAGE _ESC ( p ) } </a> ` ) ] . join ( ' / ' ) ;
const enc = ( name ) => name . split ( '/' ) . map ( encodeURIComponent ) . join ( '/' ) ;
const kind = ( n ) => ( /\.(png|jpe?g|gif|webp|avif|svg)$/i . test ( n ) ? 'img' : /\.(mp3|m4a|aac|ogg|oga|opus|flac|wav)$/i . test ( n ) ? 'audio' : /\.(mp4|webm|mov|m4v)$/i . test ( n ) ? 'video' : '' ) ;
const rows = [
... listing . folders . map ( ( f ) => ` <li class="dir"><a href=" ${ enc ( f ) } ">📁 ${ PAGE _ESC ( f . replace ( /\/$/ , '' ) ) } </a></li> ` ) ,
... listing . files . map ( ( f ) => {
const k = kind ( f . name ) ;
const media = k === 'img' ? ` <img loading="lazy" src=" ${ enc ( f . name ) } " alt=""> ` : k ? ` <button type="button" class="play" data-src=" ${ enc ( f . name ) } " data-kind=" ${ k } ">▶</button> ` : '' ;
return ` <li> ${ media } <a href=" ${ enc ( f . name ) } "> ${ PAGE _ESC ( f . name ) } </a><span class="sz"> ${ fmtSize ( f . size ) } </span></li> ` ;
} ) ,
] . join ( '' ) ;
const more = listing . next ? ` <p><a href="?token= ${ encodeURIComponent ( listing . next ) } ">More…</a></p> ` : '' ;
return ` <!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
< title > $ { PAGE _ESC ( parts . at ( - 1 ) || bucket ) } · $ { PAGE _ESC ( owner ) } < / t i t l e >
< style > body { margin : 0 ; background : # 0b100 e ; color : # e6ece9 ; font : 15 px / 1.55 system - ui , - apple - system , "Segoe UI" , sans - serif } main { max - width : 860 px ; margin : 0 auto ; padding : 28 px 18 px 120 px }
a { color : # 3 ddc97 ; text - decoration : none } a : hover { text - decoration : underline } . owner { color : # 8e9 c96 ; font - size : 13 px } h1 { font - size : 20 px ; margin : 4 px 0 18 px ; font - weight : 600 } h1 span { color : # e6ece9 }
ul { list - style : none ; padding : 0 ; margin : 0 ; border : 1 px solid rgba ( 255 , 255 , 255 , . 08 ) ; border - radius : 10 px ; overflow : hidden } li { display : flex ; align - items : center ; gap : 10 px ; padding : 9 px 14 px ; border - top : 1 px solid rgba ( 255 , 255 , 255 , . 06 ) ; min - width : 0 } li : first - child { border - top : none }
li a { flex : 1 ; min - width : 0 ; overflow - wrap : anywhere } li img { width : 44 px ; height : 44 px ; object - fit : cover ; border - radius : 6 px ; flex : none } . sz { color : # 8e9 c96 ; font - size : 13 px ; white - space : nowrap }
. play { flex : none ; width : 32 px ; height : 32 px ; border - radius : 50 % ; border : 0 ; background : # 3 ddc97 ; color : # 06231 a ; cursor : pointer ; font - size : 13 px }
# player { position : fixed ; left : 0 ; right : 0 ; bottom : 0 ; background : # 121 a17 ; border - top : 1 px solid rgba ( 255 , 255 , 255 , . 08 ) ; padding : 10 px 16 px ; display : none } # player . on { display : block } # player audio , # player video { width : 100 % ; max - height : 50 vh } . empty { color : # 8e9 c96 ; padding : 18 px } footer { color : # 5 f6b66 ; font - size : 12.5 px ; margin - top : 18 px } < / s t y l e > < / h e a d >
< body > < main > < div class = "owner" > $ { PAGE _ESC ( owner ) } · public folder < / d i v > < h 1 > $ { c r u m b s } < / h 1 >
$ { rows ? ` <ul> ${ rows } </ul> ` : '<p class="empty">This folder is empty.</p>' } $ { more }
< footer > Shared with Pithos on Silent Mode . < a href = "?json" > JSON < / a > < / f o o t e r > < / m a i n >
< div id = "player" > < / d i v >
< script > document . addEventListener ( 'click' , function ( e ) { var b = e . target . closest ( '.play' ) ; if ( ! b ) return ; var p = document . getElementById ( 'player' ) ; var m = document . createElement ( b . dataset . kind ) ; m . src = b . dataset . src ; m . controls = true ; m . autoplay = true ; p . innerHTML = '' ; p . appendChild ( m ) ; p . className = 'on' ; } ) ; < / s c r i p t >
< / b o d y > < / h t m l > ` ;
}
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
function json ( 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 s3Error ( res , status , code , message ) {
const esc = ( s ) => String ( s ) . replace ( /[<>&'"]/g , ( c ) => ` &# ${ c . charCodeAt ( 0 ) } ; ` ) ;
res . writeHead ( status , { 'content-type' : 'application/xml' } ) ;
res . end ( ` <?xml version="1.0" encoding="UTF-8"?><Error><Code> ${ esc ( code ) } </Code><Message> ${ esc ( message ) } </Message></Error> ` ) ;
}
function readAll ( stream ) {
return new Promise ( ( resolve , reject ) => {
const parts = [ ] ;
stream . on ( 'data' , ( c ) => parts . push ( c ) ) ;
stream . on ( 'end' , ( ) => resolve ( Buffer . concat ( parts ) . toString ( 'utf8' ) ) ) ;
stream . on ( 'error' , reject ) ;
} ) ;
}
async function jsonBody ( req ) {
let size = 0 ;
const parts = [ ] ;
for await ( const c of req ) { size += c . length ; if ( size > 1024 * 1024 ) throw httpError ( 413 , 'request too large' ) ; parts . push ( c ) ; }
try { return JSON . parse ( Buffer . concat ( parts ) . toString ( 'utf8' ) || '{}' ) ; } catch { throw httpError ( 400 , 'invalid JSON' ) ; }
}
function done ( res ) {
return new Promise ( ( resolve ) => { if ( res . writableFinished ) return resolve ( ) ; res . on ( 'finish' , resolve ) ; res . on ( 'close' , resolve ) ; } ) ;
}
function fmtBytes ( n ) {
const u = [ 'B' , 'KiB' , 'MiB' , 'GiB' , 'TiB' ] ;
let i = 0 ;
while ( n >= 1024 && i < u . length - 1 ) { n /= 1024 ; i ++ ; }
return ` ${ n . toFixed ( n < 10 && i ? 1 : 0 ) } ${ u [ i ] } ` ;
}