5f7b3b6b7ac62c27f846b5a09b05088929293fb9
16 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5f7b3b6b7a |
v1.0.0:4 — remove default admin@local credentials; require StartOS action to bootstrap
Security: shipping admin@local / workout123 as a default that the
operator was supposed-to-rotate-but-might-not is the kind of footgun
that turns into "default-credential exposure" headlines. Eliminated.
prisma/seed.ts now ONLY seeds the InstanceSettings singleton — no
admin user, no UserPreferences, no exercises in the build-time
fallback DB. The image still ships with prisma/exercises.seed.json
(curated 164-exercise library) but those rows aren't inserted until
an admin is created via the StartOS Action.
The change-admin-credentials Action now does INSERT-or-UPDATE in one
shot. CREATE mode (no admin exists) inserts the User row, inserts
UserPreferences with sensible defaults, and runs
ensureExerciseLibrary.cjs for the new admin so they don't have to
wait for the next service start to see the curated library. UPDATE
mode (admin exists) keeps the v1.0.0:1-3 rotation behavior. The
mode is auto-detected by counting `WHERE isAdmin = 1`.
The login page is now a server component that reads the admin count
upfront. Zero admins -> renders a "needs setup" panel pointing at
the StartOS Action ("Services -> Proof of Work -> Actions -> Set
admin credentials"). Otherwise renders the existing LoginForm
(extracted to LoginForm.tsx). Eliminates the
"I tried admin@local/workout123 and it failed, what's wrong"
fresh-installer confusion.
Backward compatible for upgrades from v1.0.0:1-3:
- /data already has an admin user; the no-admin detection never
triggers; login behaves identically to before.
- The Action's UPDATE mode still works for rotation.
Version graph: v1.0.0:4 promoted to current; v1.0.0:1, :2, :3 all
listed as `other` for in-place upgrade paths.
README updated to call out the explicit no-default-account design
and how to bootstrap an admin in local dev (Prisma Studio, since
the StartOS action isn't available off-StartOS).
|
||
|
|
a64fee4873 |
Replace placeholder manifest URLs with real keysat-xyz/proof-of-work
packageRepo + upstreamRepo now point at the public GitHub repo where this code will live. marketingUrl set to null (not yet a landing page). |
||
|
|
97ed07fd07 |
v1.0.0:3 — post-cutover seed strip
Removes the one-time `/data` snapshot from the deployed Docker image now that the cutover from the legacy `workout-log` package is verified done (v1.0.0:1 + :2 in production). Dockerfile - Drops `COPY start9/0.4/seed/data /app/seed/data`. - Drops the `WORKOUT_BAKED_SEED_DB_PATH` env var. - Comment block explains the rationale + how to re-seed if ever needed. docker_entrypoint.sh - Step 1 collapses to single-branch fallback: if /data is empty AND /app/prisma/data/app.db exists, copy the empty-schema fallback. The baked-seed branch is gone. - Comment cross-references v1.0.0:3 for the rationale. start9/0.4/seed/README.md rewritten to reflect historical-only status + how to re-seed for the rare "spin up another instance with this history" case. Version graph - Adds startos/versions/v1.0.0.3.ts with empty up/down migrations and release notes. - Promotes v1.0.0:3 to `current`; v1.0.0:1 and :2 move to `other` so hosts on either upgrade in place. No schema changes, no data migration. /data on existing installs is left exactly as-is. Image size drops by ~1.7MB (the snapshot size). |
||
|
|
a5df05c3ce |
Broaden gitignore to cover *.bak under seed/data/ (followup to 5f16855)
Commit 5f16855's message claimed it broadened .gitignore alongside the git rm --cached, but the .gitignore edit was left unstaged. This commit actually applies the change so future *.db.bak / *.bak files dropped into start9/*/seed/data/ stay out of version control. |
||
|
|
32b855f25b |
Untrack accidentally-committed seed/data/app.db.bak
The CSP-revert commit ba5a5d9 picked up start9/0.4/seed/data/app.db.bak because the gitignore only matched *.db, not *.db.bak. That file is the prior live snapshot (1.6MB of real workout history + bcrypt'd password hash) and must never be in version control. git rm --cached removes it from the index; the file stays on disk. .gitignore broadened to cover *.db.bak and *.bak under start9/*/seed/data/ so this can't recur. NOTE: the file is still in the previous commit. If this repo is ever pushed to a public remote, run `git filter-repo --path start9/0.4/seed/data/app.db.bak --invert-paths` (or BFG) to scrub history first. |
||
|
|
edeb1eb148 |
v1.0.0:2 — revert CSP nonces; restore inline-friendly CSP
v1.0.0:1 shipped a per-request nonce-based CSP via Next.js middleware. In production it produced a blank first paint: Next 14.2.x's bootstrap inline scripts weren't picking up the nonce reliably from the x-nonce request header, so the browser blocked them. This release reverts to the pre-experiment posture: - middleware.ts back to auth gating only (no nonce, no CSP). - next.config.js restores the static CSP with `'unsafe-inline'` allowed for script-src and style-src. Same headers (HSTS, Referrer-Policy, Permissions-Policy, frame-ancestors 'none', etc.) all stay. - New startos/versions/v1.0.0.2.ts with empty up/down migrations and a release note explaining the bug + revert. Promoted to `current` in the version graph; v1.0.0:1 moves to `other` so existing installs upgrade in place. No schema changes, no data migration. Existing v1.0.0:1 installs keep their /data. Re-attempt path documented in middleware.ts and next.config.js comments: future PR can revisit nonce CSP using Next's documented pattern verbatim (notably setting CSP on BOTH request headers and response headers — we only set it on response). |
||
|
|
990f5582b8 |
Typed Prisma queries, bcrypt native, CSP nonces, /api/me/import, more tests
Typed Prisma queries
- where: any in app/api/workouts/route.ts (GET + POST) and
lib/db/workouts.ts replaced with Prisma.WorkoutWhereInput +
Prisma.WorkoutCreateInput + Prisma.DateTimeFilter. Catches typos
at compile time and surfaces query shape directly in tooltips.
Workout import endpoint tests (tests/routes-import.test.ts)
- 7 tests covering /api/workouts/import/save: 401 unauthenticated,
empty workouts rejected, case-insensitive name matching against
existing exercises, new-exercise creation with isCustom=true and
type='other' default, explicit existingExerciseId honored over
name lookup, multiple workouts per call, sequential setNumber
per exercise per workout.
bcryptjs -> bcrypt (native)
- Roughly 10x faster than the pure-JS implementation under load —
login latency drops from ~250ms to ~25ms. Hash format is fully
cross-compatible with bcryptjs ($2a$ / $2b$ both verify), so
existing user passwords keep working without migration.
- Dockerfile builder stage adds python3 + make + g++ as a safety net
for native node-gyp compilation on alpine when prebuilt binaries
aren't available.
- Runner stage explicitly COPYs node_modules/bcrypt so the .node
binding is unambiguously present even if Next.js standalone
tracing somehow misses it.
- StartOS package's changeAdminCredentials.ts keeps bcryptjs (it's
bundled by ncc into a single JS file and runs only on the rare
admin action; native bcrypt would require shipping the .node
binding through ncc which it doesn't handle gracefully).
CSP nonces (middleware.ts + next.config.js)
- Per-request nonce generated in middleware. Forwarded to Next via
the x-nonce request header, which Next 13.4+ automatically stamps
onto its inline bootstrap scripts. CSP response header includes
`'nonce-${nonce}' 'strict-dynamic'`, dropping the previous
`'unsafe-inline'` from script-src.
- Static CSP removed from next.config.js (middleware-set headers
override static ones, so keeping both was redundant).
- Middleware matcher widened to all paths except static assets so
the CSP applies to every page response. Existing /main + /api
auth gating preserved.
- style-src keeps 'unsafe-inline' — Next/Tailwind still inject
critical inline <style>; tightening that requires hash-based
style-src or per-style nonce stamping (Next doesn't auto-do
either). Worth a follow-up if you want the cleanest possible CSP.
/api/me/import (mirror of /api/me/export)
- Accepts the same JSON shape /api/me/export emits (schema string
validated: only `proof-of-work-export@1` accepted today).
- mode: 'merge' (default) — adds imported rows; existing exercises
with matching names are NOT overwritten (the user's custom version
wins). All workout sets with a known exercise get rebound to the
user's actual exercise id via name lookup.
- mode: 'replace' — wipes the user's exercises/workouts/sets first,
then imports. Requires `confirm: "REPLACE"` in the body.
- Always scoped to the actor — never touches other users' data.
- Profile/admin flag/sessions/InstanceSettings deliberately not
imported (account identity stays put).
- 7 tests cover: 401, schema rejection, merge create+skip, replace
confirmation gate, replace wipes-then-imports, isolation across
users.
- ExportMyData component grew Import (merge) + Import (replace)
buttons with native browser confirm() before the destructive
replace.
Test suite now 81 tests across 9 files in ~2.6s.
|
||
|
|
54fa77f2eb |
Sessions UI, CSV parser tests, route tests, composite indexes, verify-db action
Per-user sessions UI (Settings -> Active sessions) - listMySessions returns the current user's still-valid sessions with last-8-char token suffix (UX hint) and an isCurrent flag (the authoritative "this device" marker). - revokeSession refuses if the target is the actor's current token — use Sign out for that flow. Per-row Revoke button on every other. - revokeAllOtherSessions = the previously-internal `deleteOtherSessions` helper exposed as a single button "Sign out other devices". - All gated to the actor's own userId (never lets a user touch another user's sessions). CSV parser refactor + tests - Extracted parseCSV, NAME_MAP, parseFloatMaybe, parseIntMaybe, getVariationNote, resolveExerciseName, parseDate from app/api/import/parse/route.ts to lib/csvParser.ts. Behavior byte-identical; route is now a thin wrapper that imports from the lib. - 18 tests covering: empty input, simple rows, lowercased headers, quoted-field commas, escaped double quotes, CRLF normalization, empty-line handling; numeric maybe-parsers; getVariationNote known patterns + null pass-through; ALL 27 NAME_MAP entries map to their canonical target; named CSV-shorthand examples; M/D/YYYY + ISO date parsing with noon-UTC anchoring (so US negative-offset zones still see the same calendar day). Workout + exercise CRUD route tests - New tests/routes-crud.test.ts: GET/POST /api/exercises, GET/POST /api/workouts. 401 on unauthenticated, per-user data isolation, query filtering, soft-delete exclusion, isCustom stamping, duplicate detection, type-driven inputFields defaults (cardio gets duration+calories), Zod validation rejection, set creation with weight/reps/rpe persisted, negative-reps rejected. - Helper builds NextRequest objects so the routes' nextUrl.searchParams access works. Composite indexes for hot query paths (schema.prisma + entrypoint) - Session: (userId, expiresAt) for "list my still-valid sessions" and per-user cleanup. - Workout: (userId, deletedAt, date) for the workout list query (filter by user + alive + date order). - SetLog: (workoutId, setNumber) for the always-ordered set fetch under each workout. - Existing single-column indexes kept; composites are additive. - Entrypoint runs CREATE INDEX IF NOT EXISTS so live snapshots pick up the new indexes on first boot after upgrade. verify-database StartOS action (start9/0.4/startos/actions/verifyDatabase.ts) - Read-only. Runs PRAGMA integrity_check + quick_check + row-count queries against /data/app.db, reports as a structured result. - allowedStatuses: only-running. Mounts the volume read-only. - Use after a StartOS Backup, after a host crash, or after a fresh sideload to confirm the data is sound before relying on it. Test suite now 67 tests across 7 files in ~2.4s. |
||
|
|
5de974edaf |
ESLint, server-action tests, export-my-data, enriched healthcheck, CHANGELOG
ESLint
- Pinned eslint@^8 + eslint-config-next@^14 to match Next 14's `next lint`.
ESLint 9's flat-config breaks `next lint` for legacy projects.
- .eslintrc.json extends next/core-web-vitals; ignores tests/, scripts/,
prisma/data/, .next/, node_modules.
- 7 pre-existing warnings surfaced (exhaustive-deps + alt-text + img tag
in user-written components). Left as warnings — pre-existing, not
breaking. CI runs lint; warnings don't fail the job.
Server action tests (tests/actions-admin.test.ts, tests/actions-auth.test.ts)
- Vitest setup file (tests/helpers/setup-actions.ts) sets DATABASE_URL
to a per-process temp SQLite DB and runs `prisma db push` BEFORE
lib/prisma instantiates its global PrismaClient. Tests then call the
real server actions against an isolated DB.
- vi.mock + vi.hoisted to mock @/lib/auth.getCurrentUser, next/headers
cookies+headers, next/navigation redirect, next/cache revalidatePath.
- Coverage:
- admin: setUserAdmin (Forbidden, promote, last-admin demote refused,
demote-with-other-admin allowed), deleteUser (last-admin guard,
self-delete refused, cascading delete to exercises + workouts),
adminResetPassword (hash-and-revoke, short-password rejected).
- auth flows: signupAction (closed by default, opens-and-creates,
mismatched confirm rejected, short pwd rejected, malformed email
rejected, no email-enumeration leak), changePasswordAction
(rotate-and-revoke-others, wrong current pwd rejected, no-op pwd
rejected), deleteMyAccountAction (phrase required, password required,
last-admin refused, success cascades + clears cookie + redirects).
- Total suite: 34 tests, ~2s.
Export my data (/api/me/export + Settings -> Export my data)
- Downloads a JSON dump of every workout/set/exercise/program tied to
the user. Excludes password hash and sessions. Filename includes
email + date. content-disposition: attachment, no-store cache.
- Exported shape matches the underlying tables 1:1 so a future "import
my data" flow can round-trip without ambiguity.
Enriched /api/health
- Now reports: database.connected, database.journalMode (and walEnabled
shortcut), users count, instanceSettings.signupsOpen, library.available
+ sizeBytes. Surfaces a `warnings` array if journal_mode != 'wal' but
doesn't fail the check (app still works without WAL — just unsafe for
online backups). Returns 503 only on hard DB failure.
CHANGELOG.md
- Single Unreleased section documenting everything that will ship as
v1.0.0:1 once the maintainer drops a fresh /data snapshot. Added /
Changed / Removed / Compat-notes sections.
|
||
|
|
65f4b7a7c7 |
Test suite (Vitest) + GitHub Actions CI
Test suite (proof-of-work/tests/)
- vitest 4 + @vitest/coverage-v8 added as devDeps. New scripts: test,
test:watch, test:coverage.
- vitest.config.ts: single-fork pool so DB-backed tests don't trample
each other on temp file paths. `@/` alias mirrors tsconfig.
- tests/helpers/db.ts: setupTestDb() spins up a fresh schema-only
SQLite file per test suite via `prisma db push --skip-generate`,
returns a scoped PrismaClient + cleanup that removes WAL/SHM
sidecars too.
- tests/rateLimit.test.ts: under-limit / over-limit / per-key
isolation / window-slides-and-allows-again. Plus tests for
clientIpFromHeaders header preference order.
- tests/auth-pure.test.ts: hashPassword roundtrips, salt-randomness
(same input, different hash), bcrypt format ($2 prefix).
- tests/library.test.ts: actually runs the runtime
ensureExerciseLibrary.cjs against a temp DB with two users — verifies
the full library lands for every user, idempotent across two runs,
and a user's own custom exercise with a colliding name is NOT
overwritten on subsequent ensure passes. This is the highest-stakes
test in the suite (covers the exact code path that runs on every
container boot).
12 tests, ~1.0s total.
GitHub Actions CI (.github/workflows/ci.yml)
- Two jobs running in parallel on push + PR to master/main:
- `app`: cd proof-of-work && npm ci && prisma validate && prisma
generate && tsc --noEmit && npm test
- `startos`: cd start9/0.4 && npm ci && npm run check (the
StartOS package's existing tsc --noEmit script)
- Both jobs use Node 20 with npm cache keyed off the package-lock.
|
||
|
|
d51400c2a9 |
Robustness: WAL mode, security headers, last-login, delete-my-account
SQLite WAL mode (start9/0.4/docker_entrypoint.sh) - Switches journal_mode to WAL on every boot. WAL persists in the DB header so this is effectively a one-shot but rerunning is harmless. - Crucial for the "background StartOS Backup while users are using the app" case: under the default rollback journal, a long backup can capture an inconsistent snapshot. WAL keeps readers and the writer from blocking each other. - synchronous=NORMAL paired with WAL: still crash-consistent at every checkpoint, ~10x faster than FULL. Security headers (proof-of-work/next.config.js) - Content-Security-Policy with frame-ancestors 'none', base-uri 'self', form-action 'self', object-src 'none'. Keeps 'unsafe-inline' for script/style because Next.js emits inline bootstrap; tightening to nonce-based CSP is a follow-up. - Strict-Transport-Security: max-age=31536000; includeSubDomains. - Referrer-Policy: strict-origin-when-cross-origin (don't leak workout IDs etc. to third-party sites). - Permissions-Policy: deny camera, mic, geolocation, USB, etc. across the board (none of those APIs are used today; explicit deny means vulnerability scanners have one less thing to flag). Last-login tracking - New User.lastLoginAt column. createSession stamps it inside the same transaction as the new Session row. - Compat ALTER in entrypoint adds the column to legacy snapshots. - Admin Users table now shows a relative-age cell (today / Nd ago / Nmo ago / Ny ago / "never" if the user hasn't signed in since the column was added). Hover reveals the exact ISO timestamp. Self-serve delete-my-account (Settings -> Danger Zone) - Requires both the user's current password AND typing the literal phrase "delete my account" (defense against a stolen-session attacker nuking the account in one click). - Refused for the last admin (instance can't be left with no admin — the user is told to promote someone first). - Cascades through Prisma onDelete: Cascade on every relation owned by User, so workouts, exercises, sessions, preferences all go in one shot. Session cookie cleared, redirected to /auth/login. |
||
|
|
a11639cc56 |
Self-serve password change, admin user management, login/signup rate limit
Per-user password change (Settings -> Change password)
- changePasswordAction verifies current password before rotating, blocks
same-as-current, requires 8+ chars and matching confirm.
- Always revokes every other session for the user via
deleteOtherSessions(userId, currentToken). If you're rotating because
you suspect compromise, the worst-case kicks the attacker off
immediately. UI surfaces how many sessions were revoked.
- ChangePasswordForm sits between SettingsForm and AdminInstanceSettings
on the existing settings page. Available to every user, no admin
privileges required.
Admin user management (/main/admin/users — admin only)
- New page lists every account: email, name, joined date, workout count,
role. Linked from the AdminInstanceSettings panel ("Manage users ->").
- Per-row actions: Promote/Demote (toggles isAdmin), Reset password
(inline 8+ char input), Delete (cascading delete via Prisma onDelete:
Cascade — workouts, exercises, sessions, preferences all go).
- Last-admin guard: setUserAdmin and deleteUser refuse if it would
leave 0 admins. Self-delete is blocked from the admin UI (preserves
the actor's session and forces them to use a "danger zone" flow they
set up explicitly elsewhere).
- adminResetPassword force-revokes ALL of the target user's sessions —
admin reset implies the old credential is no longer trusted.
- Server actions all do their own requireAdmin() gate (defense in depth
beyond the page-level redirect).
Rate limit on /auth/login + /auth/signup
- New lib/rateLimit.ts: tiny in-process sliding-window limiter, no deps.
Map<key, timestamps[]> with cutoff filtering on each call. Per Node
process — fine for the single-replica StartOS deploy shape.
- clientIpFromHeaders prefers x-forwarded-for (leftmost), falls back to
x-real-ip, then 'unknown' (acts as a global cap in dev).
- signup: 5 attempts per IP per 15min. Cuts off automated account
spraying without blocking legitimate household-member sign-ups.
- login: 10 attempts per IP per 15min. Slows credential stuffing while
giving typo-prone users headroom.
|
||
|
|
53d2bade5c |
Use crypto.randomBytes for session tokens; add deleteOtherSessions helper
Session tokens were derived from Math.random() + Date.now() — predictable enough that a determined attacker could brute-force or guess valid tokens for other users. Switch to crypto.randomBytes(32) (256 bits of CSPRNG output, hex-encoded), the standard for opaque bearer tokens. Also adds deleteOtherSessions(userId, keepToken) so the upcoming password-change flow can log a user out of every other device when they rotate their password. |
||
|
|
d9c4e6c4a0 |
Multi-user: self-serve sign-up gated by admin-toggleable flag
Schema - User.isAdmin: Boolean default false (Prisma) - New InstanceSettings singleton (id=1) holding signupsOpen flag Boot-time compat ALTERs (docker_entrypoint.sh) - Adds User.isAdmin column to legacy snapshots; auto-promotes the oldest user to admin if no admin exists yet, so workout-log -> proof-of-work cutover preserves admin functionality with no manual SQL. - Creates InstanceSettings table + singleton row (signupsOpen=0) for any snapshot that doesn't have it. App: sign-up flow - /auth/signup page: server component that reads InstanceSettings upfront. If sign-ups are closed it shows a closed-instance message and a back-to-sign-in link rather than a dead form. If open it renders SignupForm (client) which calls signupAction (server). - signupAction: re-checks the flag (defense in depth), validates email format / 8-char password / matching confirm, blocks duplicate-email enumeration with a generic error, creates the user with isAdmin=false, seeds default UserPreferences, ensures the curated exercise library for the new user (lib/library.ts upserts every entry), then issues a session cookie. - Login page now links to /auth/signup; old "Demo: admin@example.com / password" footer (which was wrong anyway) removed. App: admin in-app toggle - Settings page renders new AdminInstanceSettings component for admins only. Optimistic toggle posts to /api/admin/signups; error rollback on failure. - /api/admin/signups: GET returns current flag (any authed user, so the UI knows whether to show the sign-up CTA later); POST flips it (admin only). StartOS package action - toggle-signups: same setter as the in-app toggle, accessible from the StartOS UI without an admin login. Single boolean input. Asserts the read-back value matches what was written before reporting success. - changeAdminCredentials now keys the UPDATE on `WHERE isAdmin = 1 ORDER BY createdAt ASC LIMIT 1` (was: just ORDER BY createdAt) — correct under multi-user. Release notes / docs - v1.0.0:1 release notes expanded to call out multi-user as part of the cutover release (no separate version needed since this is the first proof-of-work release shipping to anyone). - Root README: short Multi-user section explaining both toggle paths and that new users get the curated library automatically. - README dev setup adds `npx prisma generate` step (required after schema changes for local dev). |
||
|
|
aa407b5f67 |
Rebrand to Proof of Work; multi-user 0.4 package with curated library sync
Repo cleanup - Add top-level .gitignore (was missing; node_modules, .next, *.s9pk, image.tar, seed/data/*.db, log files, etc.) and a root README. - Delete legacy start9/0.3.5/ package (StartOS 0.3.5 wrapper, no longer the deploy target). - Delete start9-example-packaging/ (template from another project). - Delete planning docs (START9_PACKAGING_LOG.md, VERSIONING.md, STARTOS_0.4_UPGRADE_PROMPT.md, ICON_FILES_INDEX.md, etc.) — info now lives in the deploy guide and code comments. - Drop the standalone Dockerfile, docker-compose.yml, ICON_*, and dev log/build artifacts from the app dir. - Drop the v0.1.0:18/19/20 version files (they belonged to the legacy workout-log package and don't apply to the new id). Rename + new package - Rename app dir workout-planner/ -> proof-of-work/. - Rename StartOS package id workout-log -> proof-of-work; the new id makes this a brand new StartOS service (clean cutover from the old one rather than in-place upgrade). - Reset version graph; v1.0.0:1 is the seeded cutover release. The Dockerfile bakes a one-time /data snapshot and docker_entrypoint.sh copies it into the new volume on truly-fresh first boot only (both /data/app.db missing AND /data/.seeded absent). - Move start9/0.4-migration/ -> start9/0.4/; the old start9/0.4/ stub is gone. Curated exercise library (multi-user-aware) - proof-of-work/prisma/exercises.seed.json is the canonical library shipped to every install (164 exercises today, dumped from the live snapshot). - proof-of-work/scripts/sync-library.cjs (npm run sync-library) refreshes the JSON from start9/0.4/seed/data/app.db after refresh_seed.sh. - proof-of-work/prisma/seed.ts now reads from the JSON instead of a hardcoded 52-exercise array; runs at Docker build time to seed the fallback DB and on first boot for fresh installs. - proof-of-work/prisma/ensureExerciseLibrary.cjs runs on every container boot (from docker_entrypoint.sh) and INSERT OR IGNOREs every library entry for every user, keyed on (userId, name). Library updates flow to existing installs on package upgrade; user-custom exercises (isCustom=true) and any colliding names are never overwritten; removed exercises stay on existing installs (additive-only). Deploy guide (start9/0.4/DEPLOY_040.md) - Rewritten end-to-end for the workout-log -> proof-of-work cutover: refresh_seed, sync-library, build, sideload, verify, rotate creds, stop the old service, then post-cutover cleanup release v1.0.0:2. |
||
|
|
1b64c45c52 | Initial commit for Start9 packaging |