mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(desktop): remove the dead community API token plumbing
applyCommunity sent a `token` arg that the Rust apply_workspace command
stopped declaring when the relay moved to pure Nostr key auth ("Sprout
speaks Nostr. Nothing else.", #475). Tauri silently drops invoke args
the command doesn't declare, so the Community.token collected by the
add/edit dialogs was persisted to localStorage and never applied
anywhere — a secret-shaped field with no consumer. Resolve the arg
mismatch by deleting the plumbing rather than re-accepting the token:
- Stop sending `token` from applyCommunity and align its invoke args
with the Rust signature (relayUrl, nsec, reposDir).
- Remove the API Token field from the Add/Edit Community dialogs, and
drop token handling from useCommunities (update-result matrix,
dup-merge on add, reinit trigger) and the sidebar/switcher prop
types.
- Retire Community.token to a `@deprecated token?: never` marker and
strip the stale secret from persisted entries in loadCommunities
(extending the existing nsec-strip migration), so it cannot leak
from localStorage into future sessions.
- Regression tests: applyCommunity must send exactly the arg keys
apply_workspace declares (guards the silent-drop IPC contract), and
loadCommunities strips token/nsec and persists the cleaned list.
Tested: desktop pnpm test (2941 passed, incl. the 2 new regression
tests), tsc --noEmit, biome check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
f05ccd9ad1
commit
bfc0953158
@@ -269,7 +269,7 @@ function CommunityApp({ sharedIdentity }: { sharedIdentity: boolean }) {
|
||||
useNestNotifications();
|
||||
|
||||
// Composite key: changes when community ID changes OR when
|
||||
// the active community's config is updated (relayUrl/token).
|
||||
// the active community's config is updated (relayUrl/reposDir).
|
||||
const communityKey = `${activeCommunity?.id ?? "none"}-${reinitKey}`;
|
||||
|
||||
// Latch once the community key deviates from its cold-boot value: from then
|
||||
@@ -299,7 +299,6 @@ function CommunityApp({ sharedIdentity }: { sharedIdentity: boolean }) {
|
||||
id: crypto.randomUUID(),
|
||||
name: transaction.communityName,
|
||||
relayUrl: transaction.relayUrl,
|
||||
token: transaction.token,
|
||||
reposDir: transaction.reposDir,
|
||||
addedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import test from "node:test";
|
||||
|
||||
import {
|
||||
clearCommunityStorage,
|
||||
loadCommunities,
|
||||
migrateLegacyCommunityStorage,
|
||||
} from "./communityStorage.ts";
|
||||
|
||||
@@ -59,3 +60,61 @@ test("clearCommunityStorage removes new and legacy state", () => {
|
||||
|
||||
assert.equal(storage.length, 0);
|
||||
});
|
||||
|
||||
test("loadCommunities strips legacy nsec and token secrets and persists the cleaned list", () => {
|
||||
// `token` was a relay API token sent to an `apply_workspace` arg the Rust
|
||||
// command no longer declares — Tauri silently dropped it, so the secret sat
|
||||
// unused in localStorage. `nsec` was superseded by the on-disk identity.key.
|
||||
const storage = createMemoryStorage({
|
||||
"buzz-communities": JSON.stringify([
|
||||
{
|
||||
id: "ws-1",
|
||||
name: "Community A",
|
||||
relayUrl: "wss://relay-a.example.com",
|
||||
token: "buzz_legacy-secret",
|
||||
addedAt: "2024-01-01",
|
||||
},
|
||||
{
|
||||
id: "ws-2",
|
||||
name: "Community B",
|
||||
relayUrl: "wss://relay-b.example.com",
|
||||
nsec: "nsec1legacysecret",
|
||||
addedAt: "2024-01-02",
|
||||
},
|
||||
{
|
||||
id: "ws-3",
|
||||
name: "Community C",
|
||||
relayUrl: "wss://relay-c.example.com",
|
||||
addedAt: "2024-01-03",
|
||||
},
|
||||
]),
|
||||
});
|
||||
// loadCommunities reads the real localStorage global (and writes back via
|
||||
// window.localStorage), so point both at the in-memory store for the test.
|
||||
globalThis.localStorage = storage;
|
||||
const hadWindow = "window" in globalThis;
|
||||
const previousWindow = globalThis.window;
|
||||
globalThis.window = { localStorage: storage };
|
||||
try {
|
||||
const communities = loadCommunities();
|
||||
|
||||
assert.equal(communities.length, 3);
|
||||
for (const community of communities) {
|
||||
assert.equal("token" in community, false);
|
||||
assert.equal("nsec" in community, false);
|
||||
}
|
||||
// The cleaned list is persisted back so the secrets cannot leak into
|
||||
// future sessions.
|
||||
const persisted = storage.getItem("buzz-communities");
|
||||
assert.equal(persisted.includes("token"), false);
|
||||
assert.equal(persisted.includes("nsec"), false);
|
||||
assert.equal(persisted.includes("relay-b.example.com"), true);
|
||||
} finally {
|
||||
delete globalThis.localStorage;
|
||||
if (hadWindow) {
|
||||
globalThis.window = previousWindow;
|
||||
} else {
|
||||
delete globalThis.window;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -57,16 +57,24 @@ export function loadCommunities(): Community[] {
|
||||
if (!Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
// Migration: older builds stored the user's `nsec` in localStorage and
|
||||
// re-applied it to the backend on every reload, which silently overwrote
|
||||
// any `import_identity` result with the original generated key. The
|
||||
// on-disk `identity.key` file is the only source of truth now. Strip
|
||||
// any lingering `nsec` from existing entries on read and persist the
|
||||
// cleaned list back so it cannot leak into future sessions.
|
||||
// Migration: strip secrets older builds persisted but nothing reads.
|
||||
// - `nsec`: older builds stored the user's nsec in localStorage and
|
||||
// re-applied it to the backend on every reload, which silently overwrote
|
||||
// any `import_identity` result with the original generated key. The
|
||||
// on-disk `identity.key` file is the only source of truth now.
|
||||
// - `token`: a relay API token from before the relay moved to pure Nostr
|
||||
// key auth (NIP-42/NIP-98). The backend command that once consumed it
|
||||
// stopped accepting it, so the secret sat unused in localStorage.
|
||||
// Strip both from existing entries on read and persist the cleaned list
|
||||
// back so they cannot leak into future sessions.
|
||||
let didStrip = false;
|
||||
const cleaned = (parsed as Array<Record<string, unknown>>).map((entry) => {
|
||||
if (entry && typeof entry === "object" && "nsec" in entry) {
|
||||
const { nsec: _nsec, ...rest } = entry;
|
||||
if (
|
||||
entry &&
|
||||
typeof entry === "object" &&
|
||||
("nsec" in entry || "token" in entry)
|
||||
) {
|
||||
const { nsec: _nsec, token: _token, ...rest } = entry;
|
||||
didStrip = true;
|
||||
return rest;
|
||||
}
|
||||
|
||||
@@ -83,9 +83,9 @@ test("resolveCommunityUpdateResult_relay_edit_on_inactive_community_no_reinit",
|
||||
assert.deepEqual(result, { kind: "updated", requiresReinit: false });
|
||||
});
|
||||
|
||||
test("resolveCommunityUpdateResult_token_change_on_active_requires_reinit", () => {
|
||||
test("resolveCommunityUpdateResult_repos_dir_change_on_active_requires_reinit", () => {
|
||||
const result = resolveCommunityUpdateResult(COMMUNITIES, "ws-1", "ws-1", {
|
||||
token: "new-token",
|
||||
reposDir: "/Users/dev/repos",
|
||||
});
|
||||
assert.deepEqual(result, { kind: "updated", requiresReinit: true });
|
||||
});
|
||||
|
||||
@@ -2,7 +2,6 @@ export type Community = {
|
||||
id: string;
|
||||
name: string;
|
||||
relayUrl: string;
|
||||
token?: string;
|
||||
/**
|
||||
* The pubkey associated with the active identity at the time the community
|
||||
* was created. Display-only — auth always uses the persisted `identity.key`
|
||||
@@ -24,4 +23,11 @@ export type Community = {
|
||||
* authoritative private key is the on-disk `identity.key` file.
|
||||
*/
|
||||
nsec?: never;
|
||||
/**
|
||||
* @deprecated Never read. A relay API token from before the relay moved to
|
||||
* pure Nostr key auth (NIP-42/NIP-98) — the backend stopped accepting it,
|
||||
* so the secret sat unused in localStorage. New entries never set this
|
||||
* field, and `loadCommunities()` strips it on read.
|
||||
*/
|
||||
token?: never;
|
||||
};
|
||||
|
||||
@@ -31,7 +31,6 @@ export function AddCommunityDialog({
|
||||
}: AddCommunityDialogProps) {
|
||||
const [name, setName] = React.useState("");
|
||||
const [relayUrl, setRelayUrl] = React.useState("");
|
||||
const [token, setToken] = React.useState("");
|
||||
const [inviteCode, setInviteCode] = React.useState("");
|
||||
const [reposDir, setReposDir] = React.useState("");
|
||||
const communityOnboarding = useCommunityOnboarding();
|
||||
@@ -41,7 +40,6 @@ export function AddCommunityDialog({
|
||||
onOpenChange(false);
|
||||
setName("");
|
||||
setRelayUrl("");
|
||||
setToken("");
|
||||
setInviteCode("");
|
||||
setReposDir("");
|
||||
setReposDirError(null);
|
||||
@@ -72,20 +70,11 @@ export function AddCommunityDialog({
|
||||
relayUrl: normalizedRelayUrl,
|
||||
inviteCode: inviteCode.trim() || undefined,
|
||||
communityName: name.trim() || deriveCommunityName(normalizedRelayUrl),
|
||||
token: token.trim() || undefined,
|
||||
reposDir: expandedReposDir,
|
||||
});
|
||||
handleClose();
|
||||
},
|
||||
[
|
||||
name,
|
||||
relayUrl,
|
||||
token,
|
||||
inviteCode,
|
||||
reposDir,
|
||||
communityOnboarding,
|
||||
handleClose,
|
||||
],
|
||||
[name, relayUrl, inviteCode, reposDir, communityOnboarding, handleClose],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -136,24 +125,6 @@ export function AddCommunityDialog({
|
||||
value={name}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
className="text-sm font-medium text-foreground"
|
||||
htmlFor="ws-token"
|
||||
>
|
||||
API Token
|
||||
<span className="ml-1 text-xs font-normal text-muted-foreground">
|
||||
(optional)
|
||||
</span>
|
||||
</label>
|
||||
<Input
|
||||
id="ws-token"
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
placeholder="buzz_..."
|
||||
type="password"
|
||||
value={token}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
className="text-sm font-medium text-foreground"
|
||||
|
||||
@@ -49,7 +49,7 @@ type CommunitySwitcherProps = {
|
||||
onAddCommunity: () => void;
|
||||
onUpdateCommunity: (
|
||||
id: string,
|
||||
updates: Partial<Pick<Community, "name" | "relayUrl" | "token">>,
|
||||
updates: Partial<Pick<Community, "name" | "relayUrl">>,
|
||||
) => void;
|
||||
onRemoveCommunity: (id: string) => void;
|
||||
};
|
||||
|
||||
@@ -22,9 +22,7 @@ type EditCommunityDialogProps = {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSave: (
|
||||
id: string,
|
||||
updates: Partial<
|
||||
Pick<Community, "name" | "relayUrl" | "token" | "reposDir">
|
||||
>,
|
||||
updates: Partial<Pick<Community, "name" | "relayUrl" | "reposDir">>,
|
||||
) => void;
|
||||
onRemove?: (id: string) => void;
|
||||
canRemove?: boolean;
|
||||
@@ -40,7 +38,6 @@ export function EditCommunityDialog({
|
||||
}: EditCommunityDialogProps) {
|
||||
const [name, setName] = React.useState("");
|
||||
const [relayUrl, setRelayUrl] = React.useState("");
|
||||
const [token, setToken] = React.useState("");
|
||||
const [reposDir, setReposDir] = React.useState("");
|
||||
const [reposDirError, setReposDirError] = React.useState<string | null>(null);
|
||||
|
||||
@@ -49,7 +46,6 @@ export function EditCommunityDialog({
|
||||
if (community && open) {
|
||||
setName(community.name);
|
||||
setRelayUrl(community.relayUrl);
|
||||
setToken(community.token ?? "");
|
||||
setReposDir(community.reposDir ?? "");
|
||||
setReposDirError(null);
|
||||
}
|
||||
@@ -67,7 +63,7 @@ export function EditCommunityDialog({
|
||||
}
|
||||
|
||||
const updates: Partial<
|
||||
Pick<Community, "name" | "relayUrl" | "token" | "reposDir">
|
||||
Pick<Community, "name" | "relayUrl" | "reposDir">
|
||||
> = {};
|
||||
|
||||
const trimmedName = name.trim();
|
||||
@@ -80,11 +76,6 @@ export function EditCommunityDialog({
|
||||
updates.relayUrl = normalizedUrl;
|
||||
}
|
||||
|
||||
const trimmedToken = token.trim() || undefined;
|
||||
if (trimmedToken !== community.token) {
|
||||
updates.token = trimmedToken;
|
||||
}
|
||||
|
||||
// Expand `~` to an absolute path before save — the backend rejects
|
||||
// tilde paths. An empty field clears the override (REPOS reverts to a
|
||||
// real dir). Validate the expanded value (the bytes the backend
|
||||
@@ -108,7 +99,7 @@ export function EditCommunityDialog({
|
||||
|
||||
handleClose();
|
||||
},
|
||||
[community, name, relayUrl, token, reposDir, onSave, handleClose],
|
||||
[community, name, relayUrl, reposDir, onSave, handleClose],
|
||||
);
|
||||
|
||||
const handleRemove = React.useCallback(() => {
|
||||
@@ -166,24 +157,6 @@ export function EditCommunityDialog({
|
||||
value={relayUrl}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
className="text-sm font-medium text-foreground"
|
||||
htmlFor="edit-ws-token"
|
||||
>
|
||||
API Token
|
||||
<span className="ml-1 text-xs font-normal text-muted-foreground">
|
||||
(optional)
|
||||
</span>
|
||||
</label>
|
||||
<Input
|
||||
id="edit-ws-token"
|
||||
onChange={(e) => setToken(e.target.value)}
|
||||
placeholder="buzz_..."
|
||||
type="password"
|
||||
value={token}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label
|
||||
className="text-sm font-medium text-foreground"
|
||||
|
||||
@@ -38,7 +38,7 @@ export function resolveCommunityUpdateResult(
|
||||
activeId: string | null,
|
||||
id: string,
|
||||
updates: Partial<
|
||||
Pick<Community, "name" | "relayUrl" | "token" | "pubkey" | "reposDir">
|
||||
Pick<Community, "name" | "relayUrl" | "pubkey" | "reposDir">
|
||||
>,
|
||||
): UpdateCommunityResult {
|
||||
const current = communities.find((w) => w.id === id);
|
||||
@@ -55,7 +55,6 @@ export function resolveCommunityUpdateResult(
|
||||
const hasChange =
|
||||
(updates.name !== undefined && updates.name !== current.name) ||
|
||||
(updates.relayUrl !== undefined && updates.relayUrl !== current.relayUrl) ||
|
||||
(updates.token !== undefined && updates.token !== current.token) ||
|
||||
(updates.pubkey !== undefined && updates.pubkey !== current.pubkey) ||
|
||||
(updates.reposDir !== undefined && updates.reposDir !== current.reposDir);
|
||||
|
||||
@@ -66,7 +65,6 @@ export function resolveCommunityUpdateResult(
|
||||
isActive &&
|
||||
((updates.relayUrl !== undefined &&
|
||||
updates.relayUrl !== current.relayUrl) ||
|
||||
(updates.token !== undefined && updates.token !== current.token) ||
|
||||
(updates.reposDir !== undefined &&
|
||||
updates.reposDir !== current.reposDir));
|
||||
|
||||
@@ -76,7 +74,7 @@ export function resolveCommunityUpdateResult(
|
||||
export type UseCommunitiesReturn = {
|
||||
communities: Community[];
|
||||
activeCommunity: Community | null;
|
||||
/** Counter bumped when the active community's config changes (relayUrl/token). */
|
||||
/** Counter bumped when the active community's config changes (relayUrl/reposDir). */
|
||||
reinitKey: number;
|
||||
/** Add a community, deduplicating by relayUrl. Returns the final ID in the list. */
|
||||
addCommunity: (community: Community) => string;
|
||||
@@ -88,7 +86,7 @@ export type UseCommunitiesReturn = {
|
||||
updateCommunity: (
|
||||
id: string,
|
||||
updates: Partial<
|
||||
Pick<Community, "name" | "relayUrl" | "token" | "pubkey" | "reposDir">
|
||||
Pick<Community, "name" | "relayUrl" | "pubkey" | "reposDir">
|
||||
>,
|
||||
) => UpdateCommunityResult;
|
||||
};
|
||||
@@ -154,7 +152,6 @@ function useCommunitiesInternal(): UseCommunitiesReturn {
|
||||
? {
|
||||
...w,
|
||||
name: community.name || w.name,
|
||||
token: community.token ?? w.token,
|
||||
pubkey: community.pubkey ?? w.pubkey,
|
||||
}
|
||||
: w,
|
||||
@@ -227,7 +224,7 @@ function useCommunitiesInternal(): UseCommunitiesReturn {
|
||||
(
|
||||
id: string,
|
||||
updates: Partial<
|
||||
Pick<Community, "name" | "relayUrl" | "token" | "pubkey" | "reposDir">
|
||||
Pick<Community, "name" | "relayUrl" | "pubkey" | "reposDir">
|
||||
>,
|
||||
): UpdateCommunityResult => {
|
||||
const result = resolveCommunityUpdateResult(
|
||||
|
||||
@@ -78,7 +78,7 @@ export function useCommunityInit(
|
||||
// before resetting when the user switches to a different community.
|
||||
const prevCommunityIdRef = useRef<string | null>(null);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: we intentionally depend on specific properties (id/relayUrl/token/reposDir) — depending on the whole object would trigger resets on name-only changes
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: we intentionally depend on specific properties (id/relayUrl/reposDir) — depending on the whole object would trigger resets on name-only changes
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
@@ -155,7 +155,6 @@ export function useCommunityInit(
|
||||
await applyCommunity(
|
||||
activeCommunity.relayUrl,
|
||||
undefined,
|
||||
activeCommunity.token,
|
||||
activeCommunity.reposDir,
|
||||
);
|
||||
} catch (error) {
|
||||
@@ -211,7 +210,6 @@ export function useCommunityInit(
|
||||
}, [
|
||||
activeCommunity?.id,
|
||||
activeCommunity?.relayUrl,
|
||||
activeCommunity?.token,
|
||||
activeCommunity?.reposDir,
|
||||
isSharedIdentity,
|
||||
communityKey,
|
||||
|
||||
@@ -27,7 +27,6 @@ export type CommunityOnboardingTransaction = {
|
||||
relayUrl: string;
|
||||
inviteCode?: string;
|
||||
communityName: string;
|
||||
token?: string;
|
||||
reposDir?: string;
|
||||
communityId?: string;
|
||||
createdAt: string;
|
||||
@@ -40,7 +39,6 @@ export type StartCommunityOnboardingInput = {
|
||||
relayUrl: string;
|
||||
inviteCode?: string;
|
||||
communityName?: string;
|
||||
token?: string;
|
||||
reposDir?: string;
|
||||
};
|
||||
|
||||
@@ -115,7 +113,6 @@ export function startCommunityOnboarding(
|
||||
...existing,
|
||||
inviteCode: input.inviteCode?.trim() || existing.inviteCode,
|
||||
communityName: input.communityName?.trim() || existing.communityName,
|
||||
token: input.token?.trim() || existing.token,
|
||||
reposDir: input.reposDir ?? existing.reposDir,
|
||||
updatedAt: now.toISOString(),
|
||||
error: undefined,
|
||||
@@ -132,7 +129,6 @@ export function startCommunityOnboarding(
|
||||
relayUrl,
|
||||
inviteCode: input.inviteCode?.trim() || undefined,
|
||||
communityName: input.communityName?.trim() || deriveCommunityName(relayUrl),
|
||||
token: input.token?.trim() || undefined,
|
||||
reposDir: input.reposDir,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
|
||||
@@ -130,7 +130,7 @@ type AppSidebarProps = {
|
||||
onOpenDm: (input: { pubkeys: string[] }) => Promise<void>;
|
||||
onUpdateCommunity: (
|
||||
id: string,
|
||||
updates: Partial<Pick<Community, "name" | "relayUrl" | "token">>,
|
||||
updates: Partial<Pick<Community, "name" | "relayUrl">>,
|
||||
) => void;
|
||||
onRemoveCommunity: (id: string) => void;
|
||||
onCreateAgent: () => void;
|
||||
|
||||
@@ -29,7 +29,7 @@ type CommunityRailProps = {
|
||||
onAddCommunity: () => void;
|
||||
onUpdateCommunity: (
|
||||
id: string,
|
||||
updates: Partial<Pick<Community, "name" | "relayUrl" | "token">>,
|
||||
updates: Partial<Pick<Community, "name" | "relayUrl">>,
|
||||
) => void;
|
||||
onRemoveCommunity: (id: string) => void;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ type SidebarProfileCardProps = {
|
||||
onSwitchCommunity: (id: string) => void;
|
||||
onUpdateCommunity: (
|
||||
id: string,
|
||||
updates: Partial<Pick<Community, "name" | "relayUrl" | "token">>,
|
||||
updates: Partial<Pick<Community, "name" | "relayUrl">>,
|
||||
) => void;
|
||||
profile?: Profile;
|
||||
resolvedDisplayName: string;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { applyCommunity } from "./tauri.ts";
|
||||
|
||||
/**
|
||||
* Regression test for the applyCommunity → apply_workspace arg contract.
|
||||
*
|
||||
* Tauri silently drops any invoke arg the Rust command does not declare: the
|
||||
* frontend once sent a `token` arg that `apply_workspace` had stopped
|
||||
* accepting, so the persisted Community.token was never applied — with no
|
||||
* error anywhere. Guard the exact key set against the Rust signature
|
||||
* (`apply_workspace(relay_url, nsec, repos_dir)` in
|
||||
* desktop/src-tauri/src/commands/workspace.rs) so a drifting arg fails here
|
||||
* instead of vanishing at the IPC boundary.
|
||||
*/
|
||||
test("applyCommunity sends exactly the args apply_workspace declares", async () => {
|
||||
const calls = [];
|
||||
const hadWindow = "window" in globalThis;
|
||||
const previousWindow = globalThis.window;
|
||||
globalThis.window = {
|
||||
__TAURI_INTERNALS__: {
|
||||
invoke: async (cmd, args) => {
|
||||
calls.push({ cmd, args });
|
||||
},
|
||||
},
|
||||
};
|
||||
try {
|
||||
await applyCommunity("wss://relay.example.com", undefined, "/repos");
|
||||
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].cmd, "apply_workspace");
|
||||
assert.deepEqual(Object.keys(calls[0].args).sort(), [
|
||||
"nsec",
|
||||
"relayUrl",
|
||||
"reposDir",
|
||||
]);
|
||||
assert.equal(calls[0].args.relayUrl, "wss://relay.example.com");
|
||||
assert.equal(calls[0].args.nsec, null);
|
||||
assert.equal(calls[0].args.reposDir, "/repos");
|
||||
} finally {
|
||||
if (hadWindow) {
|
||||
globalThis.window = previousWindow;
|
||||
} else {
|
||||
delete globalThis.window;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1273,16 +1273,16 @@ export async function cancelPairing(): Promise<void> {
|
||||
await invokeTauri("cancel_pairing");
|
||||
}
|
||||
|
||||
// Arg keys must match the Rust `apply_workspace` parameters exactly — Tauri
|
||||
// silently drops undeclared keys (a legacy `token` arg was lost this way).
|
||||
export async function applyCommunity(
|
||||
relayUrl: string,
|
||||
nsec?: string,
|
||||
token?: string,
|
||||
reposDir?: string,
|
||||
): Promise<void> {
|
||||
await invokeTauri("apply_workspace", {
|
||||
relayUrl,
|
||||
nsec: nsec ?? null,
|
||||
token: token ?? null,
|
||||
reposDir: reposDir ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user