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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- When a component reads multiple adjacent values from the same store hook, prefer a consolidated selector with `C.useShallow(...)` instead of multiple separate subscriptions.
- Keep types accurate. Do not use casts or misleading annotations to mask a real type mismatch just to get around an issue; fix the type or fix the implementation.
- Do not add new exported functions, types, or constants unless they are required outside the file. Prefer file-local helpers for one-off implementation details and tests.
- Do not edit lockfiles by hand. They are generated artifacts. If you cannot regenerate one locally, leave it unchanged.
- Do not use `navigation.setOptions` for header state in this repo. Pass header-driving state through route params so `getOptions` can read it synchronously, or use [`shared/stores/modal-header.tsx`](/Users/ChrisNojima/SourceCode/go/src/github.com/keybase/client/shared/stores/modal-header.tsx) when the flow already uses the shared modal header mechanism.
- Components must not mutate Zustand stores directly with `useXState.setState`, `getState()`-based writes, or similar ad hoc store mutation. If a component needs to affect store state, route it through a store dispatch action or move the state out of the store.
- During refactors, do not delete existing guards, conditionals, or platform/test-specific behavior unless you have proven they are dead and the user asked for that behavior change. Port checks like `androidIsTestDevice` forward into the new code path instead of silently dropping them.
Expand Down
40 changes: 31 additions & 9 deletions shared/common-adapters/video.shared.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,52 @@
import type * as React from 'react'
import {Box2} from './box'
import Text from './text'
import URL from 'url-parse'

const Kb = {
Box2,
Text,
}

const urlIsOK = (url: string, allowFile?: boolean) => {
const allowedHosts = ['127.0.0.1', 'localhost']
const allowedHosts = new Set(['127.0.0.1', 'localhost'])
const hasAllowedChars = (url: string) => /^[a-zA-Z0-9=.%:?/&_-]*$/.test(url)
const hasScheme = (url: string) => /^[a-z][a-z\d+.-]*:/i.test(url)
Comment on lines +10 to +12
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hasAllowedChars regex currently contains \\- inside the character class, which also allows literal backslashes (\) in addition to -. If the intent is only to allow hyphens, this broadens accepted input (e.g., UNC / backslash paths) and weakens the URL validation. Consider removing the extra backslash and placing - at the end of the character class (or escaping it as \-).

Copilot uses AI. Check for mistakes.

// This should be as limited as possible, to avoid injections.
if (/^[a-zA-Z0-9=.%:?/&-_]*$/.test(url)) {
const u = new URL(url)
if (allowedHosts.includes(u.hostname)) {
const isAllowedHostURL = (url: string) => {
try {
const parsed = new URL(url.startsWith('//') ? `http:${url}` : url)
if (allowedHosts.has(parsed.hostname.toLowerCase())) {
return true
}
} catch {}
return false
}

const isAllowedFilePath = (url: string, allowFile?: boolean) => {
if (!allowFile || url.startsWith('//')) {
return false
}

if (/^[a-z]:\//i.test(url)) {
return true
}

if (allowFile && u.hostname === '') {
if (!hasScheme(url)) {
return true
}

try {
const parsed = new URL(url)
if (parsed.protocol === 'file:' && parsed.hostname === '') {
return true
}
}
} catch {}

return false
}

const urlIsOK = (url: string, allowFile?: boolean) =>
hasAllowedChars(url) && (isAllowedHostURL(url) || isAllowedFilePath(url, allowFile))

export const useCheckURL = (children: React.ReactElement, url: string, allowFile?: boolean) => {
const ok = urlIsOK(url, allowFile)
return ok ? (
Expand Down
15 changes: 11 additions & 4 deletions shared/desktop/app/main-window.desktop.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import URL from 'url-parse'
import * as Electron from 'electron'
import * as RemoteGen from '@/constants/remote-actions'
import * as R from '@/constants/remote'
Expand Down Expand Up @@ -29,11 +28,19 @@ const setupDefaultSession = () => {
if (permission === 'fullscreen') {
return callback(true)
}
const ourURL = new URL(htmlFile)
const requestURL = new URL(webContents.getURL())

let ourPathname = ''
let requestPathname = ''
try {
ourPathname = new URL(htmlFile).pathname
requestPathname = new URL(webContents.getURL()).pathname
} catch {
return callback(false)
}

if (
permission === 'notifications' &&
requestURL.pathname.toLowerCase() === ourURL.pathname.toLowerCase()
requestPathname.toLowerCase() === ourPathname.toLowerCase()
) {
// Allow notifications
return callback(true)
Expand Down
14 changes: 0 additions & 14 deletions shared/override-d.ts/misc/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,6 @@ declare module 'fs-extra' {
export const writeJsonSync: (dst: string, o: {}) => void
}

declare module 'url-parse' {
export class URLParse {
constructor(url: string)
hostname: string
protocol: string
username: string
port: string
pathname: string
query: string
password: string
}
export default URLParse
}

declare module 'emoji-datasource-apple' {
type EmojiSkinTone = '1F3FA' | '1F3FB' | '1F3FC' | '1F3FD' | '1F3FE' | '1F3FF'
export type EmojiData = {
Expand Down
3 changes: 0 additions & 3 deletions shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"react-error-boundary": "5.0.0",
"react-is": "19.2.0",
"react-native": "0.83.4",
"react-native-gesture-handler": "3.0.0-beta.2",
"react-native-kb": "file:../rnmodules/react-native-kb",
Expand All @@ -138,7 +137,6 @@
"react-native-zoom-toolkit": "5.0.1",
"shallowequal": "1.1.0",
"uint8array-extras": "1.5.0",
"url-parse": "1.5.10",
"use-debounce": "10.1.1",
"util": "0.12.5",
"zustand": "5.0.12"
Expand All @@ -163,7 +161,6 @@
"@types/lodash-es": "4.17.12",
"@types/react": "19.2.14",
"@types/react-dom": "19.2.3",
"@types/react-is": "19.2.0",
"@types/react-measure": "2.0.12",
"@types/shallowequal": "1.1.5",
"@types/webpack-env": "1.18.8",
Expand Down
85 changes: 48 additions & 37 deletions shared/router-v2/header/index.desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from 'react'
import * as Kb from '@/common-adapters'
import * as Platform from '@/constants/platform'
import SyncingFolders from './syncing-folders'
import * as ReactIs from 'react-is'
import KB2 from '@/util/electron.desktop'
import {useConfigState} from '@/stores/config'
import type {HeaderBackButtonProps} from '@react-navigation/elements'
Expand All @@ -11,18 +10,32 @@ import type {NativeStackHeaderProps} from '@react-navigation/native-stack'
const {closeWindow, minimizeWindow, toggleMaximizeWindow} = KB2.functions

type HeaderTitleProps = {
children: string
children: React.ReactNode
tintColor?: string
}

type RawOptions = {
headerMode?: string
title?: React.ReactNode
headerTitle?: React.ReactNode | React.JSXElementConstructor<HeaderTitleProps & {params?: unknown}>
headerLeft?: React.ReactNode | ((props: HeaderBackButtonProps) => React.ReactNode)
headerRight?: React.ReactNode | ((p: {tintColor?: string}) => React.ReactNode)
headerRightActions?: React.ReactNode | React.JSXElementConstructor<object>
subHeader?: React.ReactNode | React.JSXElementConstructor<object>
headerTransparent?: boolean
headerShadowVisible?: boolean
headerBottomStyle?: Kb.Styles.StylesCrossPlatform
headerStyle?: Kb.Styles.CollapsibleStyle
}

type Options = {
headerMode?: string
title?: React.ReactNode
headerTitle?: React.ReactNode | React.JSXElementConstructor<HeaderTitleProps>
headerTitle?: React.ReactNode
headerLeft?: React.ReactNode | ((props: HeaderBackButtonProps) => React.ReactNode)
headerRight?: React.ReactNode | ((p: {tintColor?: string}) => React.ReactNode)
headerRightActions?: React.JSXElementConstructor<object>
subHeader?: React.JSXElementConstructor<object>
headerRightActions?: React.ReactNode
subHeader?: React.ReactNode
headerTransparent?: boolean
headerShadowVisible?: boolean
headerBottomStyle?: Kb.Styles.StylesCrossPlatform
Expand Down Expand Up @@ -70,12 +83,7 @@ const SystemButtons = ({isMaximized}: {isMaximized: boolean}) => {
onClick={onMinimize}
style={styles.appIconBox}
>
<Kb.Icon
color="inherit"
onClick={onMinimize}
style={styles.appIcon}
type="iconfont-app-minimize"
/>
<Kb.Icon color="inherit" onClick={onMinimize} style={styles.appIcon} type="iconfont-app-minimize" />
</Kb.ClickableBox>
<Kb.ClickableBox
className="hover_background_color_black_05 color_black_50 hover_color_black"
Expand All @@ -94,19 +102,14 @@ const SystemButtons = ({isMaximized}: {isMaximized: boolean}) => {
onClick={onCloseWindow}
style={styles.appIconBox}
>
<Kb.Icon
color="inherit"
onClick={onCloseWindow}
style={styles.appIcon}
type="iconfont-app-close"
/>
<Kb.Icon color="inherit" onClick={onCloseWindow} style={styles.appIcon} type="iconfont-app-close" />
</Kb.ClickableBox>
</Kb.Box2>
)
}

function DesktopHeader(p: Props) {
const {back, navigation, options, loggedIn, useNativeFrame, params, isMaximized} = p
const {back, navigation, options, loggedIn, useNativeFrame, isMaximized} = p
const {headerMode, title, headerTitle, headerRight, headerRightActions, subHeader} = options
const {headerTransparent, headerShadowVisible, headerBottomStyle, headerStyle, headerLeft} = options

Expand All @@ -124,30 +127,19 @@ function DesktopHeader(p: Props) {
}

if (headerTitle) {
if (React.isValidElement(headerTitle)) {
titleNode = headerTitle
} else if (ReactIs.isValidElementType(headerTitle)) {
const CustomTitle = headerTitle
const props = {params}
titleNode = <CustomTitle {...props}>{title}</CustomTitle>
}
titleNode = headerTitle
}

let rightActions: React.ReactNode = null
if (ReactIs.isValidElementType(headerRightActions)) {
const CustomActions = headerRightActions
rightActions = <CustomActions />
if (headerRightActions) {
rightActions = headerRightActions
} else if (typeof headerRight === 'function') {
rightActions = headerRight({tintColor: ''})
} else if (headerRight) {
rightActions = headerRight
}

let subHeaderNode: React.ReactNode = null
if (ReactIs.isValidElementType(subHeader)) {
const CustomSubHeader = subHeader
subHeaderNode = <CustomSubHeader />
}
const subHeaderNode = subHeader ?? null

let style: Kb.Styles.StylesCrossPlatform = null
if (headerTransparent) {
Expand Down Expand Up @@ -327,6 +319,7 @@ const styles = Kb.Styles.styleSheetCreate(

type HeaderProps = Omit<Props, 'back' | 'loggedIn' | 'useNativeFrame' | 'isMaximized'> & {
back?: NativeStackHeaderProps['back']
options: RawOptions
}

function DesktopHeaderWrapper(p: HeaderProps) {
Expand All @@ -335,18 +328,36 @@ function DesktopHeaderWrapper(p: HeaderProps) {
const loggedIn = useConfigState(s => s.loggedIn)
const isMaximized = useConfigState(s => s.windowState.isMaximized)
const {headerMode, title, headerTitle, headerRightActions, subHeader} = _options
const {headerRight, headerTransparent, headerShadowVisible, headerBottomStyle, headerStyle, headerLeft} = _options
const {headerRight, headerTransparent, headerShadowVisible, headerBottomStyle, headerStyle, headerLeft} =
_options
let headerTitleNode = headerTitle
if (typeof headerTitle === 'function') {
const HeaderTitle = headerTitle as React.JSXElementConstructor<HeaderTitleProps & {params?: unknown}>
headerTitleNode = <HeaderTitle params={params}>{title}</HeaderTitle>
}
Comment on lines +333 to +337
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

headerTitle, headerRightActions, and subHeader are typed as React.ReactNode, but this wrapper treats them as possibly being functions/components (typeof ... === 'function'). With current typings, TypeScript will flag these checks as always-false (no overlap with ReactNode), and it also obscures the real shape of options coming from React Navigation (e.g. many call sites pass headerTitle: () => <... /> or subHeader: MainBanner). Consider widening the Options types to include the function/component forms you support (and only normalize them to ReactNode after this wrapper).

Copilot uses AI. Check for mistakes.

let headerRightActionsNode = headerRightActions
if (typeof headerRightActions === 'function') {
const HeaderRightActions = headerRightActions as React.JSXElementConstructor<object>
headerRightActionsNode = <HeaderRightActions />
}

let subHeaderNode = subHeader
if (typeof subHeader === 'function') {
const SubHeader = subHeader as React.JSXElementConstructor<object>
subHeaderNode = <SubHeader />
}
const options = {
headerBottomStyle,
headerLeft,
headerMode,
headerRight,
headerRightActions,
headerRightActions: headerRightActionsNode,
headerShadowVisible,
headerStyle,
headerTitle,
headerTitle: headerTitleNode,
headerTransparent,
subHeader,
subHeader: subHeaderNode,
title,
}

Expand Down
25 changes: 0 additions & 25 deletions shared/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3555,13 +3555,6 @@
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c"
integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==

"@types/react-is@19.2.0":
version "19.2.0"
resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-19.2.0.tgz#b72a01627e4820f2333abdc9945c3daac48245e7"
integrity sha512-NP2xtcjZfORsOa4g2JwdseyEnF+wUCx25fTdG/J/HIY6yKga6+NozRBg2xR2gyh7kKYyd6DXndbq0YbQuTJ7Ew==
dependencies:
"@types/react" "*"

"@types/react-measure@2.0.12":
version "2.0.12"
resolved "https://registry.yarnpkg.com/@types/react-measure/-/react-measure-2.0.12.tgz#e8ba05057357b9529aa4115064fe7ea77549f54c"
Expand Down Expand Up @@ -10561,11 +10554,6 @@ query-string@^7.1.3:
split-on-first "^1.0.0"
strict-uri-encode "^2.0.0"

querystringify@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6"
integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==

queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
Expand Down Expand Up @@ -10635,11 +10623,6 @@ react-freeze@^1.0.0:
resolved "https://registry.yarnpkg.com/react-freeze/-/react-freeze-1.0.4.tgz#cbbea2762b0368b05cbe407ddc9d518c57c6f3ad"
integrity sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==

react-is@19.2.0:
version "19.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.0.tgz#ddc3b4a4e0f3336c3847f18b806506388d7b9973"
integrity sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==

react-is@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
Expand Down Expand Up @@ -12304,14 +12287,6 @@ uri-js@^4.2.2:
dependencies:
punycode "^2.1.0"

url-parse@1.5.10:
version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"

use-debounce@10.1.1:
version "10.1.1"
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.1.1.tgz#b08b596b60a55fd4c18b44b37fdc02f058baf30a"
Expand Down