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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@
Each `## X.Y.Z` section becomes the notes of that release, on GitHub and in the app's update
prompt. Write them for users.

## 0.2.1

- The first screen offers New vault, New account and Sign in. New account creates your vault and
your sync account in one go.
- An invite to a team opened before you've set MorphGate up shows on its first screen, with your
address filled in, and once your account exists Teams opens with the invite. You can also paste
an invite there (Joining a team?). An invite opened while sync is off opens as soon as you turn
sync on: no need to open the link again.
- MorphGate can suggest a strong master password: five random words, wherever you set one.
- With saved hosts, MorphGate opens on your Hosts page, with Quick connect beside New host.
- Hosts, groups, keys, snippets, tunnels and VPNs are tables you can search and filter, each row's
actions shown as you point at it. Hosts show whether they're connected instead of a letter, each
group says what it gives its hosts (which sit indented under it), and the sidebar lists your
open sessions first. Keys, identities and host CAs are tabs of the Keys page. Session logs can
be searched and narrowed to a vault, group or host.
- A team's page starts with what needs you (a member to confirm, a device to approve, a key to
rotate), then has tabs for its members, vaults, your SSH keys, settings and activity. New vault
and the plan sit beside Invite people, and you can invite several people at once. Teams says
per team what's waiting for you.
- When a team requires two-step verification and your account doesn't have it, MorphGate says so
and takes you to turning it on, instead of the team's vaults silently not appearing.
- Sync: every change now carries a version inside its signed envelope, and deletions are signed,
so a sync server can't give a device an older copy of an item than one it has seen. Older
versions of the app keep syncing alongside this one.
- Install and restart waits for an update check that's still running, so it installs the update
that check found.

## 0.2.0

First release.
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

32 changes: 18 additions & 14 deletions apps/desktop/e2e/appimage.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ try {
);
await s.exec(`[...document.querySelectorAll(".sidebar-nav button")].find((b) => b.innerText.startsWith("Hosts")).click(); return true`);
await s.wait(() => s.exec(`return document.querySelector(".panel.page h1")?.innerText === "Hosts" || null`), "the Hosts page");
await s.clickButton("Import…");
await s.addFrom("Import…");
await s.wait(() => s.exec(`return document.querySelectorAll(".import-list li").length === 2 || null`), "the two hosts in ~/.ssh/config");
const listed = await s.exec(`return document.querySelector(".import-hosts").innerText`);
assert.match(listed, /Match blocks are skipped/);
Expand Down Expand Up @@ -191,7 +191,7 @@ try {
writeFileSync(`${home}/.putty/sessions/router`, "HostName=10.0.0.1\nProtocol=telnet\n");
await s.exec(`[...document.querySelectorAll(".sidebar-nav button")].find((b) => b.innerText.startsWith("Hosts")).click(); return true`);
await s.wait(() => s.exec(`return document.querySelector(".panel.page h1")?.innerText === "Hosts" || null`), "the Hosts page");
await s.clickButton("Import…");
await s.addFrom("Import…");
await s.clickButton("PuTTY");
await s.wait(() => s.exec(`return document.querySelector(".import-list")?.innerText.includes("E2E PuTTY") || null`), "the PuTTY session");
let page = await s.exec(`return document.querySelector(".import-hosts").innerText`);
Expand Down Expand Up @@ -375,6 +375,7 @@ try {
// host key prompt, and the route bar says who vouched for it.
await s.exec(`[...document.querySelectorAll(".sidebar-nav button")].find((b) => b.innerText.startsWith("Keys")).click(); return true`);
await s.wait(() => s.exec(`return document.querySelector(".panel.page h1")?.innerText === "Keys" || null`), "the Keys page");
await s.pageTab("Host CAs");
await s.clickButton("New host CA");
await s.waitText("New host CA", ".panel");
await s.typeLabelled("Name", "e2e host CA");
Expand All @@ -391,7 +392,7 @@ try {
throw e;
});
assert.equal(saved, "ok", `saving the host CA: ${saved}`);
await s.wait(() => s.hasText(".panel strong", "e2e host CA"), "the host CA listed");
await s.wait(() => s.hasText(".panel .lname", "e2e host CA"), "the host CA listed");
await s.exec(`[...document.querySelectorAll(".sidebar-nav button")].find((b) => b.innerText.startsWith("Hosts")).click(); return true`);
await s.wait(() => s.exec(`return document.querySelector(".panel.page h1")?.innerText === "Hosts" || null`), "the Hosts page");
await s.clickButton("New host");
Expand Down Expand Up @@ -451,7 +452,7 @@ try {
// the key goes on the account; a refresh changes nothing; no Files for its hosts (no SFTP).
await s.exec(`[...document.querySelectorAll(".sidebar-nav button")].find((b) => b.innerText.startsWith("Hosts")).click(); return true`);
await s.wait(() => s.exec(`return document.querySelector(".panel.page h1")?.innerText === "Hosts" || null`), "the Hosts page");
await s.clickButton("Clouds…");
await s.addFrom("Clouds…");
// The page shows what to add once it has listed the sources.
await s.waitText("Connect Vallic Cloud", ".cloud-page", 30000);
await s.clickButton("Connect Vallic Cloud");
Expand Down Expand Up @@ -480,10 +481,10 @@ try {
await s.exec(`[...document.querySelectorAll(".sidebar-nav button")].find((b) => b.innerText.startsWith("Hosts")).click(); return true`);
await s.wait(() => s.exec(`return document.querySelector(".panel.page h1")?.innerText === "Hosts" || null`), "the Hosts page again");
const vallicRow = await s.exec(
`const li = [...document.querySelectorAll(".panel.page li")].find((l) => l.innerText.includes("E2E Shop · Production"));
return li ? [...li.querySelectorAll("button")].map((b) => b.innerText).join("|") : null`,
`const row = [...document.querySelectorAll(".panel.page [role=row]")].find((r) => r.innerText.includes("E2E Shop · Production"));
return row ? [...row.querySelectorAll("button")].map((b) => b.getAttribute("aria-label") || b.innerText).join("|") : null`,
);
assert.ok(vallicRow && !vallicRow.split("|").includes("Files"), `no Files for a Vallic host: ${vallicRow}`);
assert.ok(vallicRow && !vallicRow.split("|").includes("Browse files (SFTP)"), `no Files for a Vallic host: ${vallicRow}`);
// Its SSH forwards no ports: the tunnel editor doesn't offer it.
await s.exec(`[...document.querySelectorAll(".sidebar-nav button")].find((b) => b.innerText.startsWith("Tunnels")).click(); return true`);
await s.clickButton("New tunnel");
Expand Down Expand Up @@ -513,7 +514,7 @@ try {
// the stopped one without an address waits.
await s.exec(`[...document.querySelectorAll(".sidebar-nav button")].find((b) => b.innerText.startsWith("Hosts")).click(); return true`);
await s.wait(() => s.exec(`return document.querySelector(".panel.page h1")?.innerText === "Hosts" || null`), "the Hosts page");
await s.clickButton("Clouds…");
await s.addFrom("Clouds…");
// The page shows what to add once it has listed the sources.
await s.waitText("Add AWS", ".cloud-page", 30000);
await s.clickButton("Add AWS");
Expand Down Expand Up @@ -544,7 +545,7 @@ try {
// connection goes through `aws ssm start-session` (the stand-in relays it to this sshd).
await s.exec(`[...document.querySelectorAll(".sidebar-nav button")].find((b) => b.innerText.startsWith("Hosts")).click(); return true`);
await s.wait(() => s.exec(`return document.querySelector(".panel.page h1")?.innerText === "Hosts" || null`), "the Hosts page");
await s.clickButton("Clouds…");
await s.addFrom("Clouds…");
await s.waitText("123456789012", ".cloud-source", 30000);
await s.exec(
`const card = [...document.querySelectorAll(".cloud-source")].find((x) => x.innerText.includes("123456789012"));
Expand Down Expand Up @@ -585,7 +586,7 @@ try {
// for the source; its droplet (this sshd) connects.
await s.exec(`[...document.querySelectorAll(".sidebar-nav button")].find((b) => b.innerText.startsWith("Hosts")).click(); return true`);
await s.wait(() => s.exec(`return document.querySelector(".panel.page h1")?.innerText === "Hosts" || null`), "the Hosts page");
await s.clickButton("Clouds…");
await s.addFrom("Clouds…");
await s.waitText("Add DigitalOcean", ".cloud-page", 30000);
await s.clickButton("Add DigitalOcean");
await s.waitText("is signed in as e2e@example.com (E2E)", ".cloud-adding", 30000);
Expand Down Expand Up @@ -628,8 +629,8 @@ try {
await s.clickButton("Select");
const pick = (label) =>
s.exec(
`const li = [...document.querySelectorAll(".panel.page li.picking")].find((l) => l.querySelector("strong")?.innerText === arguments[0]);
if (!li) throw new Error("no row " + arguments[0]); li.click(); return true`,
`const row = [...document.querySelectorAll(".panel.page [role=row].picking")].find((r) => r.querySelector(".lname")?.innerText === arguments[0]);
if (!row) throw new Error("no row " + arguments[0]); row.click(); return true`,
label,
);
await pick("e2e-csv");
Expand All @@ -640,8 +641,8 @@ try {
await s.waitText("Moved 2 hosts.", ".select-bar-wrap", 30000);
await s.wait(
() =>
s.exec(`const section = [...document.querySelectorAll(".panel.page section")].find((x) => x.querySelector(".group-title")?.innerText.includes("E2E Lab"));
const names = section ? [...section.querySelectorAll("li strong")].map((x) => x.innerText) : [];
s.exec(`const section = [...document.querySelectorAll(".panel.page .lsection")].find((x) => x.querySelector(".lghead")?.innerText.includes("E2E Lab"));
const names = section ? [...section.querySelectorAll(".lname")].map((x) => x.innerText) : [];
return names.includes("e2e-csv") && names.includes("e2e-royal") || null`),
"both hosts in E2E Lab",
);
Expand Down Expand Up @@ -933,6 +934,9 @@ try {
await s.type("input[autocomplete=current-password]", PASSWORD);
await s.exec(`document.querySelector("form button[type=submit]").click(); return true`);
await s.wait(() => s.exec(`return document.querySelector("button.brand") ? true : null`), "the workspace", 60000);
// A vault with hosts opens on them (with none, on quick connect).
await s.wait(() => s.exec(`return document.querySelectorAll(".sidebar .row-title").length > 0 || null`), "the vault's hosts", 30000);
await s.wait(() => s.exec(`return document.querySelector(".panel.page h1")?.innerText === "Hosts" || null`), "the Hosts page as home");
assert.match(await openAbout(s), new RegExp(`Version ${next} · AppImage`));
await checkNow(s);
await s.waitText("up to date");
Expand Down
Loading