Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6f6e708
mixed ESM
bourgeoa Nov 13, 2025
6e0b966
more mixed for app
bourgeoa Nov 13, 2025
78c0558
more mixed for app and test
bourgeoa Nov 13, 2025
f3f3968
index.mjs
bourgeoa Nov 13, 2025
d5d0e75
Fix line endings in bin/solid for Docker compatibility
Nov 13, 2025
13e5054
package.json
Nov 13, 2025
0357fea
.gitattributes
Nov 13, 2025
5b65f43
test/ migrated to esm in test-esm/
Nov 13, 2025
ac4eec7
ESM Migration: Convert main lib files and lib/api to ESM
Nov 13, 2025
73b2626
ESM Migration: Complete lib/api/ and lib/handlers/ conversion
Nov 13, 2025
277633e
Add ESM test command to CI workflow
bourgeoa Nov 14, 2025
562a91b
add missing test-esm/resources
Nov 20, 2025
d2ebe52
update esm and tests
Nov 20, 2025
86da28f
add missing .mjs files
Nov 22, 2025
04fef95
missing mjs file
Nov 25, 2025
f3d0a1b
add esm entry point
Nov 25, 2025
60e2f93
missing content in ldp.mjs
Nov 25, 2025
53c8278
miscellaneous updates
Nov 25, 2025
635f7b2
updates lib/api
Nov 25, 2025
ebf457f
updates to lib/srvices
Nov 25, 2025
7bd6cdd
updates to lib/models
Nov 25, 2025
aba64d9
updates to lib/payment-pointer-discovery.mjs
Nov 25, 2025
b935bdf
updates to lib/ldp-container.mjs
Nov 25, 2025
3f536ab
updates to lib/requests/auth-request.mjs
Nov 25, 2025
beb6dad
updates to lib/rdf-notification-template.mjs
Nov 25, 2025
c668ad3
updates to lib/handlers
Nov 25, 2025
5c258b2
lib/server-config.mjs
Nov 25, 2025
b370ff7
updates .mjs
Nov 30, 2025
1bf6b36
Revert "updates .mjs"
Nov 30, 2025
fd1a323
fix critical node-forge
Nov 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
19 changes: 19 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Force bash scripts to have unix line endings
*.sh text eol=lf

# Force bin files (executable scripts) to have unix line endings
bin/* text eol=lf

# Ensure batch files on Windows keep CRLF line endings
*.bat text eol=crlf

# Binary files should not be modified
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.pdf binary
*.zip binary
*.tar.gz binary
*.tgz binary
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ jobs:
- run: npm run standard
- run: npm run validate
- run: npm run nyc
- run: npm run test-esm
# Test global install of the package
- run: npm pack .
- run: npm install -g solid-server-*.tgz
Expand Down
54 changes: 54 additions & 0 deletions bin/lib/cli-utils.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import fs from 'fs-extra';
import { red, cyan, bold } from 'colorette';
import { URL } from 'url';
import LDP from '../../lib/ldp.mjs';
import AccountManager from '../../lib/models/account-manager.mjs';
import SolidHost from '../../lib/models/solid-host.mjs';

export function getAccountManager(config, options = {}) {
const ldp = options.ldp || new LDP(config);
const host = options.host || SolidHost.from({ port: config.port, serverUri: config.serverUri });
return AccountManager.from({
host,
store: ldp,
multiuser: config.multiuser
});
}

export function loadConfig(program, options) {
let argv = {
...options,
version: program.version()
};
const configFile = argv.configFile || './config.json';
try {
const file = fs.readFileSync(configFile);
const config = JSON.parse(file);
argv = { ...config, ...argv };
} catch (err) {
if (typeof argv.configFile !== 'undefined') {
if (!fs.existsSync(configFile)) {
console.log(red(bold('ERR')), 'Config file ' + configFile + " doesn't exist.");
process.exit(1);
}
}
if (fs.existsSync(configFile)) {
console.log(red(bold('ERR')), 'config file ' + configFile + " couldn't be parsed: " + err);
process.exit(1);
}
console.log(cyan(bold('TIP')), 'create a config.json: `$ solid init`');
}
return argv;
}

export function loadAccounts({ root, serverUri, hostname }) {
const files = fs.readdirSync(root);
hostname = hostname || new URL(serverUri).hostname;
const isUserDirectory = new RegExp(`.${hostname}$`);
return files.filter(file => isUserDirectory.test(file));
}

export function loadUsernames({ root, serverUri }) {
const hostname = new URL(serverUri).hostname;
return loadAccounts({ root, hostname }).map(userDirectory => userDirectory.substr(0, userDirectory.length - hostname.length - 1));
}
44 changes: 44 additions & 0 deletions bin/lib/cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Command } from 'commander'
import loadInit from './init.mjs'
import loadStart from './start.mjs'
import loadInvalidUsernames from './invalidUsernames.mjs'
import loadMigrateLegacyResources from './migrateLegacyResources.mjs'
import loadUpdateIndex from './updateIndex.mjs'
import { spawnSync } from 'child_process'
import path from 'path'
import fs from 'fs'
import { fileURLToPath } from 'url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

export default function startCli (server) {
const program = new Command()
program.version(getVersion())

loadInit(program)
loadStart(program, server)
loadInvalidUsernames(program)
loadMigrateLegacyResources(program)
loadUpdateIndex(program)

program.parse(process.argv)
if (program.args.length === 0) program.help()
}

function getVersion () {
try {
const options = { cwd: __dirname, encoding: 'utf8' }
const { stdout } = spawnSync('git', ['describe', '--tags'], options)
const { stdout: gitStatusStdout } = spawnSync('git', ['status'], options)
const version = stdout.trim()
if (version === '' || gitStatusStdout.match('Not currently on any branch')) {
throw new Error('No git version here')
}
return version
} catch (e) {
const pkgPath = path.join(__dirname, '../../package.json')
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
return pkg.version
}
}
94 changes: 94 additions & 0 deletions bin/lib/init.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import inquirer from 'inquirer'
import fs from 'fs'
import options from './options.mjs'
import camelize from 'camelize'

let questions = options
.map((option) => {
if (!option.type) {
if (option.flag) {
option.type = 'confirm'
} else {
option.type = 'input'
}
}

option.message = option.question || option.help
return option
})

export default function (program) {
program
.command('init')
.option('--advanced', 'Ask for all the settings')
.description('create solid server configurations')
.action((opts) => {
// Filter out advanced commands
let filtered = questions
if (!opts.advanced) {
filtered = filtered.filter((option) => option.prompt)
}

// Prompt to the user
inquirer.prompt(filtered)
.then((answers) => {
manipulateEmailSection(answers)
manipulateServerSection(answers)
cleanupAnswers(answers)

// write config file
const config = JSON.stringify(camelize(answers), null, ' ')
const configPath = process.cwd() + '/config.json'

fs.writeFile(configPath, config, (err) => {
if (err) {
return console.log('failed to write config.json')
}
console.log('config created on', configPath)
})
})
.catch((err) => {
console.log('Error:', err)
})
})
}

function cleanupAnswers (answers) {
Object.keys(answers).forEach((answer) => {
if (answer.startsWith('use')) {
delete answers[answer]
}
})
}

function manipulateEmailSection (answers) {
if (answers.useEmail) {
answers.email = {
host: answers['email-host'],
port: answers['email-port'],
secure: true,
auth: {
user: answers['email-auth-user'],
pass: answers['email-auth-pass']
}
}
delete answers['email-host']
delete answers['email-port']
delete answers['email-auth-user']
delete answers['email-auth-pass']
}
}

function manipulateServerSection (answers) {
answers.server = {
name: answers['server-info-name'],
description: answers['server-info-description'],
logo: answers['server-info-logo']
}
Object.keys(answers).forEach((answer) => {
if (answer.startsWith('server-info-')) {
delete answers[answer]
}
})
}

136 changes: 136 additions & 0 deletions bin/lib/invalidUsernames.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import fs from 'fs-extra';
import Handlebars from 'handlebars';
import path from 'path';
import { getAccountManager, loadConfig, loadUsernames } from './cli-utils.js';
import { isValidUsername } from '../../lib/common/user-utils.js';
import blacklistService from '../../lib/services/blacklist-service.js';
import { initConfigDir, initTemplateDirs } from '../../lib/server-config.js';
import { fromServerConfig } from '../../lib/models/oidc-manager.js';
import EmailService from '../../lib/services/email-service.js';
import SolidHost from '../../lib/models/solid-host.js';

export default function (program) {
program
.command('invalidusernames')
.option('--notify', 'Will notify users with usernames that are invalid')
.option('--delete', 'Will delete users with usernames that are invalid')
.description('Manage usernames that are invalid')
.action(async (options) => {
const config = loadConfig(program, options);
if (!config.multiuser) {
return console.error('You are running a single user server, no need to check for invalid usernames');
}
const invalidUsernames = getInvalidUsernames(config);
const host = SolidHost.from({ port: config.port, serverUri: config.serverUri });
const accountManager = getAccountManager(config, { host });
if (options.notify) {
return notifyUsers(invalidUsernames, accountManager, config);
}
if (options.delete) {
return deleteUsers(invalidUsernames, accountManager, config, host);
}
listUsernames(invalidUsernames);
});
}

function backupIndexFile(username, accountManager, invalidUsernameTemplate, dateOfRemoval, supportEmail) {
const userDirectory = accountManager.accountDirFor(username);
const currentIndex = path.join(userDirectory, 'index.html');
const currentIndexExists = fs.existsSync(currentIndex);
const backupIndex = path.join(userDirectory, 'index.backup.html');
const backupIndexExists = fs.existsSync(backupIndex);
if (currentIndexExists && !backupIndexExists) {
fs.renameSync(currentIndex, backupIndex);
createNewIndexAcl(userDirectory);
createNewIndex(username, invalidUsernameTemplate, dateOfRemoval, supportEmail, currentIndex);
console.info(`index.html updated for user ${username}`);
}
}

function createNewIndex(username, invalidUsernameTemplate, dateOfRemoval, supportEmail, currentIndex) {
const newIndexSource = invalidUsernameTemplate({
username,
dateOfRemoval,
supportEmail
});
fs.writeFileSync(currentIndex, newIndexSource, 'utf-8');
}

function createNewIndexAcl(userDirectory) {
const currentIndexAcl = path.join(userDirectory, 'index.html.acl');
const backupIndexAcl = path.join(userDirectory, 'index.backup.html.acl');
const currentIndexSource = fs.readFileSync(currentIndexAcl, 'utf-8');
const backupIndexSource = currentIndexSource.replace(/index.html/g, 'index.backup.html');
fs.writeFileSync(backupIndexAcl, backupIndexSource, 'utf-8');
}

async function deleteUsers(usernames, accountManager, config, host) {
const oidcManager = fromServerConfig({ ...config, host });
const deletingUsers = usernames.map(async username => {
try {
const user = accountManager.userAccountFrom({ username });
await oidcManager.users.deleteUser(user);
} catch (error) {
if (error.message !== 'No email given') {
throw error;
}
}
const userDirectory = accountManager.accountDirFor(username);
await fs.remove(userDirectory);
});
await Promise.all(deletingUsers);
console.info(`Deleted ${deletingUsers.length} users succeeded`);
}

function getInvalidUsernames(config) {
const usernames = loadUsernames(config);
return usernames.filter(username => !isValidUsername(username) || !blacklistService.validate(username));
}

function listUsernames(usernames) {
if (usernames.length === 0) {
return console.info('No invalid usernames was found');
}
console.info(`${usernames.length} invalid usernames were found:${usernames.map(username => `\n- ${username}`)}`);
}

async function notifyUsers(usernames, accountManager, config) {
const twoWeeksFromNow = Date.now() + 14 * 24 * 60 * 60 * 1000;
const dateOfRemoval = (new Date(twoWeeksFromNow)).toLocaleDateString();
const { supportEmail } = config;
updateIndexFiles(usernames, accountManager, dateOfRemoval, supportEmail);
await sendEmails(config, usernames, accountManager, dateOfRemoval, supportEmail);
}

async function sendEmails(config, usernames, accountManager, dateOfRemoval, supportEmail) {
if (config.email && config.email.host) {
const configPath = initConfigDir(config);
const templates = initTemplateDirs(configPath);
const users = await Promise.all(await usernames.map(async username => {
const emailAddress = await accountManager.loadAccountRecoveryEmail({ username });
const accountUri = accountManager.accountUriFor(username);
return { username, emailAddress, accountUri };
}));
const emailService = new EmailService(templates.email, config.email);
const sendingEmails = users
.filter(user => !!user.emailAddress)
.map(user => emailService.sendWithTemplate('invalid-username', {
to: user.emailAddress,
accountUri: user.accountUri,
dateOfRemoval,
supportEmail
}));
const emailsSent = await Promise.all(sendingEmails);
console.info(`${emailsSent.length} emails sent to users with invalid usernames`);
return;
}
console.info('You have not configured an email service.');
console.info('Please set it up to send users email about their accounts');
}

function updateIndexFiles(usernames, accountManager, dateOfRemoval, supportEmail) {
const invalidUsernameFilePath = path.join(process.cwd(), 'default-views', 'account', 'invalid-username.hbs');
const source = fs.readFileSync(invalidUsernameFilePath, 'utf-8');
const invalidUsernameTemplate = Handlebars.compile(source);
usernames.forEach(username => backupIndexFile(username, accountManager, invalidUsernameTemplate, dateOfRemoval, supportEmail));
}
Loading
Loading