Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,22 @@

Versions before 1.0 may include breaking changes in minor releases. Patch releases contain compatible fixes. Release candidates are intended for evaluation with synthetic data or a separate representative copy.

## [0.1.7]

### Fixed

- Setup requests now time out after 60 seconds per HTTP exchange with guidance to check the installation state before retrying. Long-running import and takeover jobs continue on the server.
- An app process waiting for setup detects when another process has initialized its database, blocks application access and requests a restart instead of offering setup again. Concurrent local setup requests are rejected while a setup operation is active.
- Setup jobs log their start, progress and completion with a shared diagnostic reference without exposing uploaded data, credentials or job tokens.
- TOTP sign-in explains the active method and presents recovery codes as a smaller emergency option below the main actions. Switching methods clears the input and focuses the code field; the new text is translated into all 14 supported languages.
- Add spacing between the sysPass XML import hint and the preview button.

### Upgrade notes

- No database schema change is introduced. Preserve existing deployment keys, secrets, persistent volumes and backups, and run a single application process.
- If setup reports `SETUP_STATE_CHANGED`, stop duplicate app instances and restart the intended application with the existing database, data volume and secrets. A browser timeout does not cancel a server-side setup job.
- The cumulative security catalogue retains NP-2026-001 and NP-2026-002; this release adds no newly disclosed security corrections.

## [0.1.6]

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ All browser assets are served locally. There are no CDNs, remote fonts, external

## Status and security responsibilities

nodePass is version **0.1.6**. Implemented features and historical local testing do not constitute a production approval. Before deployment, require **Required release checks** (`release-gate`) to pass on the exact commit, test migration and restore with a representative copy of your own data, and validate your real integrations and proxy configuration. Automated scanners are not an independent security audit. No independent external security audit is claimed.
nodePass is version **0.1.7**. Implemented features and historical local testing do not constitute a production approval. Before deployment, require **Required release checks** (`release-gate`) to pass on the exact commit, test migration and restore with a representative copy of your own data, and validate your real integrations and proxy configuration. Automated scanners are not an independent security audit. No independent external security audit is claimed.

nodePass is **not end-to-end encrypted**. The running server can decrypt secrets for authorized operations. The application host, deployment key and operators with infrastructure access are part of the trust boundary. Keep keys separate from backups and protect the host accordingly.

Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ Reports concern nodePass. The upstream sysPass project's contact address is not

## Version and verification policy

The current project version is 0.1.6. Report against the exact commit when possible. No long-term support branches or fixed support periods are promised. Production deployment requires the exact commit's **Required release checks** and operator-specific validation; local reports alone are insufficient.
The current project version is 0.1.7. Report against the exact commit when possible. No long-term support branches or fixed support periods are promised. Production deployment requires the exact commit's **Required release checks** and operator-specific validation; local reports alone are insufficient.

Read the [security model](docs/SECURITY.md) for trust boundaries and known constraints, and [Operations](docs/OPERATIONS.md#release-and-hosting-checks) for release requirements. Automated scans do not constitute an independent security audit.
10 changes: 10 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ Mount inputs read-only and make them readable only by app UID 65532. Keep sensit
- Follow [Updates and upgrades](UPGRADING.md) for the maintenance procedure, including Coolify/Dokploy image selection, backup/restore, startup migrations and post-update checks.
- After an incomplete takeover, preserve the partial DB and restore the original backup separately before pointing sysPass at it. See [rollback](MIGRATION.md#verification-and-rollback).

### Setup stops responding or reappears after import

Setup is selected by the server's `/api/v1/status` response, not by browser display preferences or saved passkeys. An old session cookie can require another sign-in, and a passkey from a deleted installation needs registration in the new installation; neither changes the database's initialization flag.

The browser bounds each setup HTTP exchange to 60 seconds. Import and takeover jobs can run longer while their status requests continue to respond. A response timeout ends the browser's wait only: the server job may still be active. Reload to check the installation state before submitting again, and preserve the database, data volume and deployment secrets. If legacy data has already been imported, continue takeover without uploading the dump again.

Compare `/api/v1/status`, `/health/live` and `/health/ready` on the same public origin. If status reports `setupRequired: true` after a completed setup, verify that the domain routes to the intended single app process and database, and check container restarts, volume mappings and the deployment key. If even liveness does not respond, inspect the app container and ingress before retrying imports. Setup logs contain `setup.started`, `setup.progress`, `setup.completed` and `setup.failed` events correlated by a reference, without uploaded data, credentials or job tokens. Keep the last phase and reference when reporting a failure.

An app process that was waiting for setup rechecks the database before reporting status or accepting setup requests. If another process initialized the database meanwhile, it returns HTTP 503 with `SETUP_STATE_CHANGED`, logs `setup.state.changed` and blocks application access instead of offering setup again. Liveness stays available and readiness reports `restart-required`. Stop duplicate app instances and restart the intended single app with the same database, volume and secrets. This guard does not add support for active replicas.

## Release and hosting checks

Follow the [release procedure](RELEASES.md) for version preparation, repository rules, approval, immutable assets and digest-pinned deployment. Release candidates never advance the stable `latest` image tag.
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nodepass",
"version": "0.1.6",
"version": "0.1.7",
"private": true,
"description": "Self-hosted sysPass successor with a Feathers 5 API and Vue 3 UI",
"license": "GPL-3.0-or-later",
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nodepass/contracts",
"version": "0.1.6",
"version": "0.1.7",
"private": true,
"type": "module",
"exports": {
Expand Down
2 changes: 1 addition & 1 deletion packages/contracts/src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const version = "0.1.6";
export const version = "0.1.7";
4 changes: 2 additions & 2 deletions packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nodepass/server",
"version": "0.1.6",
"version": "0.1.7",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -17,7 +17,7 @@
"@feathersjs/koa": "^5.0.35",
"@feathersjs/schema": "^5.0.35",
"@koa/router": "^15.7.0",
"@nodepass/contracts": "0.1.6",
"@nodepass/contracts": "0.1.7",
"@simplewebauthn/server": "^13.2.0",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
Expand Down
92 changes: 75 additions & 17 deletions packages/server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ export async function createApp(
let status = maintenance
? { installed: false, legacy: false, ready: false }
: await migration.status();
let setupStateChanged = false;
let freshSetupActive = false;
const changedSetupState = () =>
new Unavailable(
"The database was initialized after this app process started. Restart this app and verify that only one app instance uses this database. Keep the database, data volume and deployment secrets.",
{ code: "SETUP_STATE_CHANGED" },
);
let updates = new Updates(
db,
integrations,
Expand Down Expand Up @@ -251,6 +258,13 @@ export async function createApp(
throw new Forbidden("Untrusted proxy headers");
if (config.production && !ctx.secure && !ctx.path.startsWith("/health/"))
throw new Forbidden("HTTPS is required");
if (
ctx.path === "/api/v1/status" ||
ctx.path.startsWith("/api/v1/setup/")
)
await reconcileSetupState();
if (setupStateChanged && application && !restoreStatus)
throw changedSetupState();
if (
ctx.path.startsWith("/api/v1/setup/") &&
!["GET", "HEAD"].includes(ctx.method)
Expand Down Expand Up @@ -334,10 +348,16 @@ export async function createApp(
].includes(error.data?.code)
? { code: error.data.code }
: {}),
...(error instanceof Unavailable && error.data?.code === "MAINTENANCE"
? { code: "MAINTENANCE" }
...(error instanceof Unavailable &&
["MAINTENANCE", "SETUP_STATE_CHANGED"].includes(error.data?.code)
? { code: error.data.code }
: {}),
};
if (
error instanceof Unavailable &&
error.data?.code === "SETUP_STATE_CHANGED"
)
ctx.body.message = error.message;
if (ctx.status >= 500)
console.error(
JSON.stringify({
Expand Down Expand Up @@ -422,14 +442,39 @@ export async function createApp(
};
const setupJobs = new Map<string, SetupJob>(),
setupTasks = new Set<Promise<void>>();
const canReconcileSetup = () =>
!maintenance &&
!status.ready &&
!setupStateChanged &&
!freshSetupActive &&
!setupTasks.size;
const reconcileSetupState = async () => {
if (!canReconcileSetup()) return;
const current = await migration.status();
// A concurrent local setup can complete while the status query is in flight.
if (!canReconcileSetup()) return;
if (current.ready) {
setupStateChanged = true;
console.error(JSON.stringify({ event: "setup.state.changed" }));
} else status = current;
};
const queueSetup = (kind: string, work: () => Promise<unknown>): SetupJob => {
if ([...setupJobs.values()].some((job) => job.state === "running"))
if (
freshSetupActive ||
[...setupJobs.values()].some((job) => job.state === "running")
)
throw new Conflict("A setup job is already active");
if (setupJobs.size >= 5) setupJobs.delete(setupJobs.keys().next().value!);
const job: SetupJob = { id: token(), kind, step: kind, state: "running" };
const reference = randomUUID();
const logProgress = (event: string) =>
console.info(JSON.stringify({ event, reference, kind, step: job.step }));
setupJobs.set(job.id, job);
logProgress("setup.started");
migration.onProgress = (step) => {
if (step === job.step) return;
job.step = step;
logProgress("setup.progress");
};
const task = (async () => {
try {
Expand All @@ -438,11 +483,12 @@ export async function createApp(
await startJobs();
job.state = "complete";
job.step = "saved";
logProgress("setup.completed");
} catch (error) {
job.state = "failed";
job.error = {
...setupFailure(error),
reference: randomUUID(),
reference,
step: job.step,
};
console.error(
Expand Down Expand Up @@ -486,13 +532,18 @@ export async function createApp(
ctx.body = { status: "database-unavailable" };
return;
}
ctx.status = !maintenance && status.ready && vault.unlocked ? 200 : 503;
ctx.status =
!maintenance && !setupStateChanged && status.ready && vault.unlocked
? 200
: 503;
ctx.body = {
status: maintenance
? "maintenance"
: ctx.status === 200
? "ready"
: "setup-required",
: setupStateChanged
? "restart-required"
: ctx.status === 200
? "ready"
: "setup-required",
};
});
router.get("/api/v1/status", async (ctx) => {
Expand Down Expand Up @@ -528,15 +579,22 @@ export async function createApp(
await setup(ctx);
const data = body(ctx);
exact(data, ["login", "name", "password"]);
await migration.fresh({
login: text(data, "login", 50),
name: text(data, "name", 80),
password: text(data, "password"),
token: ctx.get("x-setup-token"),
});
status = await migration.status();
await startJobs();
ctx.body = { ready: status.ready };
if (freshSetupActive || setupTasks.size)
throw new Conflict("A setup job is already active");
freshSetupActive = true;
try {
await migration.fresh({
login: text(data, "login", 50),
name: text(data, "name", 80),
password: text(data, "password"),
token: ctx.get("x-setup-token"),
});
status = await migration.status();
await startJobs();
ctx.body = { ready: status.ready };
} finally {
freshSetupActive = false;
}
});
router.post("/api/v1/setup/migrate", async (ctx) => {
await setup(ctx);
Expand Down
4 changes: 2 additions & 2 deletions packages/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nodepass/web",
"version": "0.1.6",
"version": "0.1.7",
"private": true,
"type": "module",
"scripts": {
Expand All @@ -10,7 +10,7 @@
"rtk:build": "rtk npm run build"
},
"dependencies": {
"@nodepass/contracts": "0.1.6",
"@nodepass/contracts": "0.1.7",
"@simplewebauthn/browser": "^13.2.0",
"@vitejs/plugin-vue": "^6.0.0",
"@zxcvbn-ts/core": "^3.0.4",
Expand Down
Loading
Loading