add encrypted Nostr session sharing (#8922)

Signed-off-by: callebtc <93376500+callebtc@users.noreply.github.com>
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Signed-off-by: Michael Neale <michael.neale@gmail.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
callebtc
2026-05-12 22:06:44 -05:00
committed by GitHub
parent 8c36ba86c6
commit dbbee1cdbf
17 changed files with 1469 additions and 27 deletions
+147
View File
@@ -3145,6 +3145,50 @@
]
}
},
"/sessions/import/nostr": {
"post": {
"tags": [
"Session Management"
],
"operationId": "import_session_nostr",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ImportSessionNostrRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Nostr shared session imported successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Session"
}
}
}
},
"400": {
"description": "Bad request - Invalid Nostr share link"
},
"401": {
"description": "Unauthorized - Invalid or missing API key"
},
"500": {
"description": "Internal server error"
}
},
"security": [
{
"api_key": []
}
]
}
},
"/sessions/insights": {
"get": {
"tags": [
@@ -3656,6 +3700,61 @@
]
}
},
"/sessions/{session_id}/share/nostr": {
"post": {
"tags": [
"Session Management"
],
"operationId": "share_session_nostr",
"parameters": [
{
"name": "session_id",
"in": "path",
"description": "Unique identifier for the session",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ShareSessionNostrRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Session shared to Nostr successfully",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ShareSessionNostrResponse"
}
}
}
},
"401": {
"description": "Unauthorized - Invalid or missing API key"
},
"404": {
"description": "Session not found"
},
"500": {
"description": "Internal server error"
}
},
"security": [
{
"api_key": []
}
]
}
},
"/sessions/{session_id}/user_recipe_values": {
"put": {
"tags": [
@@ -5625,6 +5724,17 @@
}
}
},
"ImportSessionNostrRequest": {
"type": "object",
"required": [
"deeplink"
],
"properties": {
"deeplink": {
"type": "string"
}
}
},
"ImportSessionRequest": {
"type": "object",
"required": [
@@ -8105,6 +8215,43 @@
}
}
},
"ShareSessionNostrRequest": {
"type": "object",
"properties": {
"relays": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"ShareSessionNostrResponse": {
"type": "object",
"required": [
"deeplink",
"nevent",
"eventId",
"relays"
],
"properties": {
"deeplink": {
"type": "string"
},
"eventId": {
"type": "string"
},
"nevent": {
"type": "string"
},
"relays": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"SlashCommand": {
"type": "object",
"required": [
+18 -9
View File
@@ -8,7 +8,7 @@ import {
useLocation,
useSearchParams,
} from 'react-router-dom';
import { openSharedSessionFromDeepLink } from './sessionLinks';
import { openSharedSessionFromDeepLink, importNostrSessionFromDeepLink } from './sessionLinks';
import { type SharedSessionDetails } from './sharedSessions';
import { ErrorUI } from './components/ErrorBoundary';
import { ExtensionInstallModal } from './components/ExtensionInstallModal';
@@ -428,6 +428,11 @@ export function AppInner() {
setIsLoadingSharedSession(true);
setSharedSessionError(null);
try {
if (link.startsWith('goose://sessions/nostr')) {
await importNostrSessionFromDeepLink(link);
navigate('/sessions');
return;
}
await openSharedSessionFromDeepLink(link, (_view: View, options?: ViewOptions) => {
navigate('/shared-session', { state: options });
});
@@ -438,14 +443,18 @@ export function AppInner() {
action: 'open_shared_session',
recoverable: true,
});
// Navigate to shared session view with error
const shareToken = link.replace('goose://sessions/', '');
const options = {
sessionDetails: null,
error: errorMessage(error, 'Unknown error'),
shareToken,
};
navigate('/shared-session', { state: options });
if (link.startsWith('goose://sessions/nostr')) {
toast.error(`Failed to import Nostr session: ${errorMessage(error, 'Unknown error')}`);
navigate('/sessions');
} else {
const shareToken = link.replace('goose://sessions/', '');
const options = {
sessionDetails: null,
error: errorMessage(error, 'Unknown error'),
shareToken,
};
navigate('/shared-session', { state: options });
}
} finally {
setIsLoadingSharedSession(false);
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+82
View File
@@ -593,6 +593,10 @@ export type ImportAppResponse = {
name: string;
};
export type ImportSessionNostrRequest = {
deeplink: string;
};
export type ImportSessionRequest = {
json: string;
};
@@ -1368,6 +1372,17 @@ export type SetupResponse = {
success: boolean;
};
export type ShareSessionNostrRequest = {
relays?: Array<string>;
};
export type ShareSessionNostrResponse = {
deeplink: string;
eventId: string;
nevent: string;
relays: Array<string>;
};
export type SlashCommand = {
command: string;
command_type: CommandType;
@@ -4091,6 +4106,37 @@ export type ImportSessionResponses = {
export type ImportSessionResponse = ImportSessionResponses[keyof ImportSessionResponses];
export type ImportSessionNostrData = {
body: ImportSessionNostrRequest;
path?: never;
query?: never;
url: '/sessions/import/nostr';
};
export type ImportSessionNostrErrors = {
/**
* Bad request - Invalid Nostr share link
*/
400: unknown;
/**
* Unauthorized - Invalid or missing API key
*/
401: unknown;
/**
* Internal server error
*/
500: unknown;
};
export type ImportSessionNostrResponses = {
/**
* Nostr shared session imported successfully
*/
200: Session;
};
export type ImportSessionNostrResponse = ImportSessionNostrResponses[keyof ImportSessionNostrResponses];
export type GetSessionInsightsData = {
body?: never;
path?: never;
@@ -4473,6 +4519,42 @@ export type UpdateSessionNameResponses = {
200: unknown;
};
export type ShareSessionNostrData = {
body: ShareSessionNostrRequest;
path: {
/**
* Unique identifier for the session
*/
session_id: string;
};
query?: never;
url: '/sessions/{session_id}/share/nostr';
};
export type ShareSessionNostrErrors = {
/**
* Unauthorized - Invalid or missing API key
*/
401: unknown;
/**
* Session not found
*/
404: unknown;
/**
* Internal server error
*/
500: unknown;
};
export type ShareSessionNostrResponses = {
/**
* Session shared to Nostr successfully
*/
200: ShareSessionNostrResponse;
};
export type ShareSessionNostrResponse2 = ShareSessionNostrResponses[keyof ShareSessionNostrResponses];
export type UpdateSessionUserRecipeValuesData = {
body: UpdateSessionUserRecipeValuesRequest;
path: {
@@ -11,6 +11,8 @@ import {
Trash2,
Download,
Upload,
Share2,
LoaderCircle,
ExternalLink,
Copy,
Puzzle,
@@ -28,18 +30,29 @@ import { Skeleton } from '../ui/skeleton';
import { toast } from 'react-toastify';
import { ConfirmationModal } from '../ui/ConfirmationModal';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/Tooltip';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '../ui/dialog';
import {
deleteSession,
exportSession,
forkSession,
importSession,
importSessionNostr,
listSessions,
searchSessions,
shareSessionNostr,
Session,
updateSessionName,
ExtensionConfig,
ExtensionData,
} from '../../api';
import { getTunnelStatus } from '../../api/sdk.gen';
import { formatExtensionName } from '../settings/extensions/subcomponents/ExtensionList';
import { getSearchShortcutText } from '../../utils/keyboardShortcuts';
import { shouldShowNewChatTitle } from '../../sessions';
@@ -55,6 +68,11 @@ const i18n = defineMessages({
sessionUpdateFailed: { id: 'sessions.toast.updateFailed', defaultMessage: 'Failed to update session description: {error}' },
chatHistory: { id: 'sessions.chatHistory', defaultMessage: 'Chat history' },
importSession: { id: 'sessions.import', defaultMessage: 'Import Session' },
importNostrSession: { id: 'sessions.importNostr', defaultMessage: 'Import Link' },
importNostrTitle: { id: 'sessions.importNostr.title', defaultMessage: 'Import Nostr Session' },
importNostrDesc: { id: 'sessions.importNostr.description', defaultMessage: 'Paste a Goose Nostr share link to fetch, decrypt, and import the session.' },
importNostrPlaceholder: { id: 'sessions.importNostr.placeholder', defaultMessage: 'goose://sessions/nostr?nevent=...&key=...' },
importing: { id: 'sessions.importing', defaultMessage: 'Importing...' },
chatHistoryDesc: { id: 'sessions.chatHistoryDesc', defaultMessage: 'View and search your past conversations with Goose. {shortcut} to search.' },
searchPlaceholder: { id: 'sessions.searchPlaceholder', defaultMessage: 'Search history...' },
errorLoading: { id: 'sessions.error.loading', defaultMessage: 'Error Loading Sessions' },
@@ -73,12 +91,19 @@ const i18n = defineMessages({
importSuccess: { id: 'sessions.toast.imported', defaultMessage: 'Session imported successfully' },
importFailed: { id: 'sessions.toast.importFailed', defaultMessage: 'Failed to import session: {error}' },
exportSuccess: { id: 'sessions.toast.exported', defaultMessage: 'Session exported successfully' },
shareNostrSuccess: { id: 'sessions.toast.shareNostr', defaultMessage: 'Encrypted Nostr share link created' },
shareNostrFailed: { id: 'sessions.toast.shareNostrFailed', defaultMessage: 'Failed to create Nostr share link: {error}' },
copied: { id: 'sessions.toast.copied', defaultMessage: 'Copied to clipboard' },
openInNewWindow: { id: 'sessions.action.openNewWindow', defaultMessage: 'Open in new window' },
editSessionName: { id: 'sessions.action.editName', defaultMessage: 'Edit session name' },
duplicateSession: { id: 'sessions.action.duplicate', defaultMessage: 'Duplicate session' },
deleteSession: { id: 'sessions.action.delete', defaultMessage: 'Delete session' },
exportSession: { id: 'sessions.action.export', defaultMessage: 'Export session' },
shareNostrSession: { id: 'sessions.action.shareNostr', defaultMessage: 'Share encrypted Nostr link' },
extensions: { id: 'sessions.extensions', defaultMessage: 'Extensions:' },
shareNostrTitle: { id: 'sessions.shareNostr.title', defaultMessage: 'Encrypted Nostr Share Link' },
shareNostrDesc: { id: 'sessions.shareNostr.description', defaultMessage: 'Anyone with this link can fetch and decrypt the session. Treat it like a secret.' },
close: { id: 'sessions.close', defaultMessage: 'Close' },
});
function getSessionExtensionNames(extensionData: ExtensionData): string[] {
@@ -265,6 +290,14 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
const [sessionToDelete, setSessionToDelete] = useState<Session | null>(null);
const [showImportLinkModal, setShowImportLinkModal] = useState(false);
const [nostrImportLink, setNostrImportLink] = useState('');
const [isImportingNostr, setIsImportingNostr] = useState(false);
const [shareLink, setShareLink] = useState('');
const [showShareLinkModal, setShowShareLinkModal] = useState(false);
const [sharingSessionId, setSharingSessionId] = useState<string | null>(null);
const [nostrEnabled, setNostrEnabled] = useState(true);
// Search state for debouncing
const [searchTerm, setSearchTerm] = useState('');
const [caseSensitive, setCaseSensitive] = useState(false);
@@ -338,6 +371,17 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
loadSessions();
}, [loadSessions]);
// Hide Nostr sharing when tunnel is disabled (restricted/enterprise bundles)
useEffect(() => {
getTunnelStatus()
.then(({ data }) => {
if (data?.state === 'disabled') {
setNostrEnabled(false);
}
})
.catch(() => {});
}, []);
// Timing logic to prevent flicker between skeleton and content on initial load
useEffect(() => {
if (!isLoading && showSkeleton) {
@@ -542,10 +586,62 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
toast.success(intl.formatMessage(i18n.exportSuccess));
}, [intl]);
const handleShareSessionNostr = useCallback(
async (session: Session, e: React.MouseEvent) => {
e.stopPropagation();
setSharingSessionId(session.id);
try {
const response = await shareSessionNostr({
path: { session_id: session.id },
body: {},
throwOnError: true,
});
setShareLink(response.data.deeplink);
setShowShareLinkModal(true);
toast.success(intl.formatMessage(i18n.shareNostrSuccess));
} catch (error) {
toast.error(intl.formatMessage(i18n.shareNostrFailed, { error: errorMessage(error, 'Unknown error') }));
} finally {
setSharingSessionId(null);
}
},
[intl]
);
const handleImportClick = useCallback(() => {
fileInputRef.current?.click();
}, []);
const handleImportNostrLink = useCallback(async () => {
const deeplink = nostrImportLink.trim();
if (!deeplink) return;
setIsImportingNostr(true);
try {
await importSessionNostr({
body: { deeplink },
throwOnError: true,
});
setNostrImportLink('');
setShowImportLinkModal(false);
toast.success(intl.formatMessage(i18n.importSuccess));
await loadSessions();
} catch (error) {
toast.error(intl.formatMessage(i18n.importFailed, { error: errorMessage(error, 'Unknown error') }));
} finally {
setIsImportingNostr(false);
}
}, [intl, loadSessions, nostrImportLink]);
const handleCopyShareLink = useCallback(async () => {
try {
await navigator.clipboard.writeText(shareLink);
toast.success(intl.formatMessage(i18n.copied));
} catch (error) {
toast.error(`Failed to copy: ${errorMessage(error, 'Unknown error')}`);
}
}, [intl, shareLink]);
const handleImportSession = useCallback(
async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
@@ -586,14 +682,18 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
onDuplicateClick,
onDeleteClick,
onExportClick,
onShareClick,
onOpenInNewWindow,
isSharing,
}: {
session: Session;
onEditClick: (session: Session) => void;
onDuplicateClick: (session: Session) => void;
onDeleteClick: (session: Session) => void;
onExportClick: (session: Session, e: React.MouseEvent) => void;
onShareClick: (session: Session, e: React.MouseEvent) => void;
onOpenInNewWindow: (session: Session, e: React.MouseEvent) => void;
isSharing: boolean;
}) {
const handleEditClick = useCallback(
(e: React.MouseEvent) => {
@@ -630,6 +730,13 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
[onExportClick, session]
);
const handleShareClick = useCallback(
(e: React.MouseEvent) => {
onShareClick(session, e);
},
[onShareClick, session]
);
const handleOpenInNewWindowClick = useCallback(
(e: React.MouseEvent) => {
onOpenInNewWindow(session, e);
@@ -736,6 +843,20 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
>
<Download className="w-3 h-3 text-text-secondary hover:text-text-primary" />
</button>
{nostrEnabled && (
<button
onClick={handleShareClick}
disabled={isSharing}
className="p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer disabled:cursor-wait disabled:opacity-60"
title={intl.formatMessage(i18n.shareNostrSession)}
>
{isSharing ? (
<LoaderCircle className="w-3 h-3 text-text-secondary animate-spin" />
) : (
<Share2 className="w-3 h-3 text-text-secondary hover:text-text-primary" />
)}
</button>
)}
</div>
</Card>
);
@@ -828,7 +949,9 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
onDuplicateClick={handleDuplicateSession}
onDeleteClick={handleDeleteSession}
onExportClick={handleExportSession}
onShareClick={handleShareSessionNostr}
onOpenInNewWindow={handleOpenInNewWindow}
isSharing={sharingSessionId === session.id}
/>
))}
</div>
@@ -855,15 +978,28 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
<div className="flex flex-col page-transition">
<div className="flex justify-between items-center mb-1">
<h1 className="text-4xl font-light">{intl.formatMessage(i18n.chatHistory)}</h1>
<Button
onClick={handleImportClick}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<Upload className="w-4 h-4" />
{intl.formatMessage(i18n.importSession)}
</Button>
<div className="flex items-center gap-2">
{nostrEnabled && (
<Button
onClick={() => setShowImportLinkModal(true)}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<Share2 className="w-4 h-4" />
{intl.formatMessage(i18n.importNostrSession)}
</Button>
)}
<Button
onClick={handleImportClick}
variant="outline"
size="sm"
className="flex items-center gap-2"
>
<Upload className="w-4 h-4" />
{intl.formatMessage(i18n.importSession)}
</Button>
</div>
</div>
<p className="text-sm text-text-secondary mb-4">
{intl.formatMessage(i18n.chatHistoryDesc, { shortcut: getSearchShortcutText() })}
@@ -957,6 +1093,83 @@ const SessionListView: React.FC<SessionListViewProps> = React.memo(
onSave={handleModalSave}
/>
<Dialog open={showImportLinkModal} onOpenChange={setShowImportLinkModal}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Share2 className="w-5 h-5" />
{intl.formatMessage(i18n.importNostrTitle)}
</DialogTitle>
<DialogDescription>{intl.formatMessage(i18n.importNostrDesc)}</DialogDescription>
</DialogHeader>
<textarea
value={nostrImportLink}
onChange={(event) => setNostrImportLink(event.target.value)}
placeholder={intl.formatMessage(i18n.importNostrPlaceholder)}
className="min-h-28 w-full resize-none rounded-lg border border-border-primary bg-background-primary p-3 text-sm text-text-primary outline-none focus:ring-2 focus:ring-border-active"
disabled={isImportingNostr}
/>
<DialogFooter>
<Button
variant="outline"
onClick={() => setShowImportLinkModal(false)}
disabled={isImportingNostr}
>
{intl.formatMessage(i18n.cancel)}
</Button>
<Button
onClick={handleImportNostrLink}
disabled={isImportingNostr || !nostrImportLink.trim()}
>
{isImportingNostr ? (
<>
<LoaderCircle className="w-4 h-4 animate-spin" />
{intl.formatMessage(i18n.importing)}
</>
) : (
intl.formatMessage(i18n.importSession)
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog open={showShareLinkModal} onOpenChange={setShowShareLinkModal}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Share2 className="w-5 h-5" />
{intl.formatMessage(i18n.shareNostrTitle)}
</DialogTitle>
<DialogDescription>{intl.formatMessage(i18n.shareNostrDesc)}</DialogDescription>
</DialogHeader>
<div className="relative rounded-lg border border-border-primary bg-background-secondary p-3 pr-12">
<code className="block max-h-36 overflow-y-auto break-all text-sm text-text-primary">
{shareLink}
</code>
<Button
variant="ghost"
size="sm"
className="absolute right-2 top-2"
onClick={handleCopyShareLink}
disabled={!shareLink}
>
<Copy className="h-4 w-4" />
<span className="sr-only">{intl.formatMessage(i18n.copied)}</span>
</Button>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setShowShareLinkModal(false)}>
{intl.formatMessage(i18n.close)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<ConfirmationModal
isOpen={showDeleteConfirmation}
title={intl.formatMessage(i18n.deleteTitle)}
+36
View File
@@ -3908,6 +3908,9 @@
"sessions.action.openNewWindow": {
"defaultMessage": "Open in new window"
},
"sessions.action.shareNostr": {
"defaultMessage": "Share encrypted Nostr link"
},
"sessions.cancel": {
"defaultMessage": "Cancel"
},
@@ -3917,6 +3920,9 @@
"sessions.chatHistoryDesc": {
"defaultMessage": "View and search your past conversations with Goose. {shortcut} to search."
},
"sessions.close": {
"defaultMessage": "Close"
},
"sessions.delete.message": {
"defaultMessage": "Are you sure you want to delete the session \"{name}\"? This action cannot be undone."
},
@@ -3947,6 +3953,21 @@
"sessions.import": {
"defaultMessage": "Import Session"
},
"sessions.importNostr": {
"defaultMessage": "Import Link"
},
"sessions.importNostr.description": {
"defaultMessage": "Paste a Goose Nostr share link to fetch, decrypt, and import the session."
},
"sessions.importNostr.placeholder": {
"defaultMessage": "goose://sessions/nostr?nevent=...&key=..."
},
"sessions.importNostr.title": {
"defaultMessage": "Import Nostr Session"
},
"sessions.importing": {
"defaultMessage": "Importing..."
},
"sessions.loadingMore": {
"defaultMessage": "Loading more sessions..."
},
@@ -3965,6 +3986,15 @@
"sessions.searchPlaceholder": {
"defaultMessage": "Search history..."
},
"sessions.shareNostr.description": {
"defaultMessage": "Anyone with this link can fetch and decrypt the session. Treat it like a secret."
},
"sessions.shareNostr.title": {
"defaultMessage": "Encrypted Nostr Share Link"
},
"sessions.toast.copied": {
"defaultMessage": "Copied to clipboard"
},
"sessions.toast.deleteFailed": {
"defaultMessage": "Failed to delete session \"{name}\": {error}"
},
@@ -3986,6 +4016,12 @@
"sessions.toast.imported": {
"defaultMessage": "Session imported successfully"
},
"sessions.toast.shareNostr": {
"defaultMessage": "Encrypted Nostr share link created"
},
"sessions.toast.shareNostrFailed": {
"defaultMessage": "Failed to create Nostr share link: {error}"
},
"sessions.toast.updateFailed": {
"defaultMessage": "Failed to update session description: {error}"
},
+2 -2
View File
@@ -511,7 +511,7 @@ app.on('open-url', async (_event, url) => {
if (process.platform !== 'win32') {
const parsedUrl = new URL(url);
log.info('[Main] Received open-url event:', url);
log.info('[Main] Received open-url event:', url.includes('key=') ? url.replace(/key=[^&]+/, 'key=REDACTED') : url);
await app.whenReady();
@@ -540,7 +540,7 @@ app.on('open-url', async (_event, url) => {
// For extension/session URLs, store the deep link for processing after React is ready
pendingDeepLink = url;
log.info('[Main] Stored pending deep link for processing after React ready:', url);
log.info('[Main] Stored pending deep link for processing after React ready:', url.includes('key=') ? url.replace(/key=[^&]+/, 'key=REDACTED') : url);
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
+12
View File
@@ -1,6 +1,18 @@
import { fetchSharedSessionDetails, SharedSessionDetails } from './sharedSessions';
import { View, ViewOptions } from './utils/navigationUtils';
import { errorMessage } from './utils/conversionUtils';
import { importSessionNostr } from './api';
/**
* Imports a session from an encrypted Nostr deep link.
* Separated from shared-session handling so callers can route independently.
*/
export async function importNostrSessionFromDeepLink(url: string): Promise<void> {
await importSessionNostr({
body: { deeplink: url },
throwOnError: true,
});
}
/**
* Handles opening a shared session from a deep link