chore: v2.8.15 — Oh My Pi login sharing, omp --allow-home, Markdown Preview default, first-login password

- omp-share.js: an admin's switch (S3) runs omp's own auth broker + gateway as
  the host on loopback; a member's omp pane reaches it as a scoped client keyed
  by its pane token (clients file = live omp panes only), runs on
  --model agenthub/<model>, never holds a credential.
- omp catalog: --allow-home, so a workspace at the account's home keeps its
  sessions there (they were recorded under a temp dir and dropped).
- Settings → Appearance → Open Markdown as: Preview (default) or Raw, for
  Files/Changes, the config viewer and a chat's file panel.
- First-login password change and password rules, avatar URLs, Artifacts
  "Mine" includes installed shared skills/plugins, a member's own hook pages.
This commit is contained in:
2026-10-22 10:04:30 +07:00
parent 8a6dc6a29e
commit 750d74a2af
66 changed files with 3021 additions and 696 deletions
+31 -4
View File
@@ -18,14 +18,18 @@
* `/account` (server mode) is the signed-in account itself for Settings →
* Account: GET its profile, workspace/session counts and logins; PATCH its
* display name and email; `POST /password` changes its own password (the old
* one required, throttled like a step-up; every OTHER login ends);
* one required, throttled like a step-up; every OTHER login ends; the new one
* must pass every rule of `auth.passwordProblems` and differ from the old —
* EWEAKPASSWORD with the failed `rules` — and it clears the "pick your own
* password" flag an admin's create/reset sets);
* `POST /sign-out-others` ends every login but this one. These writes take the
* account from the login COOKIE only — a pane token (an agent acting as its
* owner) may not change the owner's login. Each change is audited.
* `/avatar` (server mode) is the account's own picture (server/lib/avatars.js):
* GET serves it (`nosniff`, `sandbox` CSP), PUT takes the raw image bytes
* (PNG/JPEG/GIF/WebP, at most 1 MB) and DELETE removes it — both writes are
* account writes like the profile's. The sidebar shows it beside the name.
* (PNG/JPEG/GIF/WebP, at most 1 MB), `POST /avatar/url` downloads the same
* from an https link (public addresses only, server/lib/avatar-url.js) and
* DELETE removes it — all writes are account writes like the profile's. The sidebar shows it beside the name.
*
* S3 (one Linux user per account): an account's dark/light choice also becomes
* the theme in their own Claude Code settings, written as them.
@@ -63,6 +67,7 @@ const PREF_KEYS = [
"agenthub.tour",
"agenthub.chatUi",
"agenthub.workspace.allTabs",
"agenthub.markdown.view",
];
const MAX_VALUE = 8 * 1024;
@@ -170,7 +175,11 @@ const ACCOUNT_STATUS = {
EBADFIELD: 400,
EBADEMAIL: 400,
EBADPASSWORD: 400,
EWEAKPASSWORD: 400,
EBADIMAGE: 400,
EBADURL: 400,
EPRIVATEURL: 400,
EFETCH: 502,
ETOOBIG: 413,
EREAUTH: 403,
ETOOMANY: 429,
@@ -206,7 +215,10 @@ function accountWrite(handler) {
} catch (err) {
const status = ACCOUNT_STATUS[err.code];
if (!status) return res.status(500).json({ error: { message: err.message } });
res.status(status).json({ error: { code: err.code, message: err.message } });
// EWEAKPASSWORD names the rules the password failed.
res.status(status).json({
error: { code: err.code, message: err.message, ...(err.rules && { rules: err.rules }) },
});
}
};
}
@@ -233,6 +245,10 @@ router.post(
sameOriginGuard,
accountWrite(async (req, user, body) => {
await auth.confirmPassword(user, body.currentPassword);
// The account's own choice: every rule, and not the one it has now. This
// also clears a "pick your own password" flag an admin's reset left.
auth.validateOwnPassword(body.newPassword, user.username);
await auth.assertNotCurrent(user.id, body.newPassword);
await auth.setPassword(user.username, body.newPassword, { keepToken: auth.cookieToken(req) });
return { out: { ok: true }, action: "own password changed" };
})
@@ -273,6 +289,17 @@ router.put(
})
);
// The same picture from a link: the server downloads it (server/lib/avatar-url.js).
router.post(
"/avatar/url",
sameOriginGuard,
accountWrite(async (_req, user, body) => {
const buf = await require("../lib/avatar-url").fetchAvatarImage(body.url);
avatars.setAvatar(user.id, buf);
return { out: accountView(user.id), action: "own avatar set from a link" };
})
);
router.delete(
"/avatar",
sameOriginGuard,