Prepare time-bound Setup URI integration for Commonlib release

Add Time-bound and Compatible generation to the Obsidian dialogue, browser peer check, and setup tools, with boundary and compatibility coverage. Show the fixed window end before sharing a Time-bound URI.

This review branch depends on unpublished @vrtmrz/livesync-commonlib 0.1.32-next.0. The root npm pin and Deno lockfile remain unchanged until that candidate is available; a fresh install of this commit is not yet expected to build.
This commit is contained in:
vorotamoroz
2026-09-28 10:22:28 +00:00
parent 2c2b9c90e4
commit 3a478cce3f
23 changed files with 1361 additions and 119 deletions
@@ -419,6 +419,38 @@ describe("runCommand abnormal cases", () => {
expect(appliedSettings.useIndexedDBAdapter).toBe(false);
});
it("setup opens Ephemeral in its window and leaves settings untouched afterwards", async () => {
const end = Date.parse("2026-10-01T00:00:00Z");
const clock = vi.spyOn(Date, "now").mockReturnValue(end - 1_000);
const passphrase = "time-bound-passphrase";
try {
const { uri, usableUntil } = await processSetting.encodeTimeBoundSetupURI(
{ ...DEFAULT_SETTINGS, isConfigured: true, couchDB_DBNAME: "time-bound-vault" },
passphrase
);
expect(usableUntil).toBe(end);
const inWindow = createCoreMock();
inWindow.services.context.standardIo.prompt.mockResolvedValue(passphrase);
await runCommand(makeOptions("setup", [uri.trim()]), { ...context, core: inWindow });
expect(inWindow.services.setting.applyExternalSettings).toHaveBeenCalledWith(
expect.objectContaining({ couchDB_DBNAME: "time-bound-vault" }),
true
);
clock.mockReturnValue(end);
const outOfWindow = createCoreMock();
outOfWindow.services.context.standardIo.prompt.mockResolvedValue(passphrase);
await expect(runCommand(makeOptions("setup", [uri.trim()]), { ...context, core: outOfWindow })).rejects.toThrow(
"Cannot open Setup URI"
);
expect(outOfWindow.services.setting.applyExternalSettings).not.toHaveBeenCalled();
expect(outOfWindow.services.control.applySettings).not.toHaveBeenCalled();
} finally {
clock.mockRestore();
}
});
it("setup imports managed TURN through the existing encrypted URI", async () => {
const core = createCoreMock();
const profiles = {
+71 -4
View File
@@ -1,7 +1,8 @@
<script lang="ts">
import type { P2PServerInfo } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
import qrcode from "qrcode-generator";
import { onDestroy, tick } from "svelte";
import { onDestroy, onMount, tick } from "svelte";
import { isTimeBoundSetupURIUsableNow } from "@vrtmrz/livesync-commonlib/setup-uri";
import {
generateP2PCheckSetup,
@@ -106,6 +107,7 @@
let elapsedMilliseconds = $state(0);
let copied = $state<"uri" | "passphrase">();
let copyError = $state("");
let currentTime = $state(Date.now());
let freshCheckStarting = $state(false);
let additionalDeviceAttempt = $state<AdditionalDeviceAttempt>();
@@ -132,6 +134,12 @@
);
let elapsedSeconds = $derived(Math.floor(elapsedMilliseconds / 1_000));
let targetLabel = $derived(target === "desktop" ? "desktop" : "mobile");
let setupURIUsable = $derived(
setup !== undefined && currentTime >= 0 && isTimeBoundSetupURIUsableNow(setup.setupURIUsableUntil)
);
let remainingMinutes = $derived(
setup ? Math.max(0, Math.ceil((setup.setupURIUsableUntil - currentTime) / 60_000)) : 0
);
let additionalElapsedMilliseconds = $derived(
additionalDeviceAttempt
? Math.max(
@@ -178,6 +186,7 @@
const generated = await generateP2PCheckSetup(target, { relay });
qrDataURL = createQRCodeDataURL(generated.setupURI);
setup = generated;
currentTime = Date.now();
} catch (error) {
preparationError = formatError(error);
} finally {
@@ -215,6 +224,11 @@
async function copyText(value: string, kind: "uri" | "passphrase"): Promise<void> {
copyError = "";
if (kind === "uri" && (!setup || !isTimeBoundSetupURIUsableNow(setup.setupURIUsableUntil))) {
currentTime = Date.now();
copyError = "This Setup URI is outside its time window. Start a fresh check.";
return;
}
try {
await navigator.clipboard.writeText(value);
copied = kind;
@@ -254,6 +268,7 @@
async function startAdditionalDeviceAttempt(): Promise<void> {
if (
!setupURIUsable ||
!monitorActive ||
outcome !== "connected" ||
activeConnections === 0 ||
@@ -268,6 +283,38 @@
await showSetupQRCode();
}
function formatSetupURIEnd(usableUntil: number): string {
return new Intl.DateTimeFormat(undefined, {
year: "numeric",
month: "short",
day: "numeric",
weekday: "short",
hour: "numeric",
minute: "2-digit",
second: "2-digit",
timeZoneName: "short",
}).format(new Date(usableUntil));
}
function openSetupURI(event: MouseEvent): void {
if (!setup || !isTimeBoundSetupURIUsableNow(setup.setupURIUsableUntil)) {
event.preventDefault();
currentTime = Date.now();
}
}
onMount(() => {
const refresh = () => (currentTime = Date.now());
const timer = setInterval(refresh, 1_000);
window.addEventListener("focus", refresh);
document.addEventListener("visibilitychange", refresh);
return () => {
clearInterval(timer);
window.removeEventListener("focus", refresh);
document.removeEventListener("visibilitychange", refresh);
};
});
onDestroy(() => {
if (elapsedTimer !== undefined) {
clearInterval(elapsedTimer);
@@ -372,6 +419,7 @@
<div class="setup-grid">
<div class="qr-panel">
{#if setupURIUsable}
<img
src={qrDataURL}
alt={additionalDeviceAttempt
@@ -383,6 +431,12 @@
? "This is the original encrypted Setup URI; it was not regenerated."
: "QR contains the encrypted Setup URI only."}
</p>
{:else}
<p role="alert">This Setup URI is outside its time window. Start a fresh check to set up another device. A device that already imported it can still be monitored below.</p>
<button type="button" onclick={startFreshCheck} disabled={freshCheckStarting}>
{freshCheckStarting ? "Starting…" : "Start a fresh check"}
</button>
{/if}
</div>
<div class="credential-panel">
@@ -401,6 +455,16 @@
</div>
<p class="field-help">Type this when LiveSync asks to decrypt the Setup URI.</p>
<p class="field-help">
{#if setupURIUsable}
Ephemeral Setup URI: usable until {formatSetupURIEnd(setup.setupURIUsableUntil)}
({remainingMinutes < 1 ? "less than 1 minute" : `${remainingMinutes} minutes`} remaining).
{:else}
This Setup URI is outside its time window. Generate a new one before sharing it.
{/if}
</p>
{#if setupURIUsable}
<label for="setup-uri">Setup URI</label>
<textarea
id="setup-uri"
@@ -414,8 +478,9 @@
<button type="button" onclick={() => copyText(setup!.setupURI, "uri")}>
{copied === "uri" ? "Copied URI" : "Copy Setup URI"}
</button>
<a class="button-link" href={setup.setupURI}>Open in Obsidian</a>
<a class="button-link" href={setup.setupURI} onclick={openSetupURI}>Open in Obsidian</a>
</div>
{/if}
{#if copyError}
<p class="inline-error" role="alert">{copyError}</p>
{/if}
@@ -550,9 +615,11 @@
class="primary-action"
type="button"
onclick={startAdditionalDeviceAttempt}
disabled={activeConnections === 0}
disabled={!setupURIUsable || activeConnections === 0}
>
{activeConnections === 0
{!setupURIUsable
? "Setup URI window ended; start a fresh check"
: activeConnections === 0
? "Waiting for the first device to reconnect…"
: "Try another device without resetting"}
</button>
+15 -4
View File
@@ -1,4 +1,4 @@
import { encodeSettingsToSetupURI } from "@vrtmrz/livesync-commonlib/compat/API/processSetting";
import { encodeTimeBoundSetupURI } from "@vrtmrz/livesync-commonlib/setup-uri";
import { compatGlobal } from "@vrtmrz/livesync-commonlib/compat/common/coreEnvFunctions";
import {
P2P_DEFAULT_SETTINGS,
@@ -17,6 +17,7 @@ export type P2PCheckTarget = "desktop" | "mobile";
export interface GeneratedP2PCheckSetup {
readonly target: P2PCheckTarget;
readonly setupURI: string;
readonly setupURIUsableUntil: number;
readonly setupPassphrase: string;
readonly groupId: string;
readonly relay: string;
@@ -147,16 +148,26 @@ export async function generateP2PCheckSetup(
browserSettings.suspendParseReplicationResult = true;
const setupPassphrase = generateSetupPassphrase();
const setupURI = await encodeSettingsToSetupURI(
const { uri: setupURI, usableUntil } = await encodeTimeBoundSetupURI(
deviceSettings,
setupPassphrase,
["pluginSyncExtendedSetting", "doNotUseFixedRevisionForChunks", "P2P_DevicePeerName", "deviceAndVaultName"],
true
{
mode: "ephemeral",
removeProperties: [
"pluginSyncExtendedSetting",
"doNotUseFixedRevisionForChunks",
"P2P_DevicePeerName",
"deviceAndVaultName",
],
skipDefaultValue: true,
}
);
if (usableUntil === null) throw new Error("Ephemeral Setup URI has no time window");
return {
target,
setupURI: setupURI.trim(),
setupURIUsableUntil: usableUntil,
setupPassphrase,
groupId: credentials.groupId,
relay: deviceSettings.P2P_relays,