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
4 changes: 4 additions & 0 deletions src/components/layout/verification-mode-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export function VerificationModePanel({ pathname }: VerificationModePanelProps)
ariaLabel: requireTranslationValue(t.pages.verification_mode_aria_label, "pages.verification_mode_aria_label"),
title: requireTranslationValue(t.pages.verification_mode_title, "pages.verification_mode_title"),
description: requireTranslationValue(t.pages.verification_mode_description, "pages.verification_mode_description"),
scope: requireTranslationValue(t.pages.verification_mode_scope, "pages.verification_mode_scope"),
on: requireTranslationValue(t.pages.verification_mode_on, "pages.verification_mode_on"),
off: requireTranslationValue(t.pages.verification_mode_off, "pages.verification_mode_off"),
browserLocalTool: requireTranslationValue(t.pages.verification_mode_browser_local_tool, "pages.verification_mode_browser_local_tool"),
Expand Down Expand Up @@ -199,6 +200,9 @@ export function VerificationModePanel({ pathname }: VerificationModePanelProps)
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
{copy.description}
</p>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
{copy.scope}
</p>
</div>
<div className="flex items-center gap-1">
<Button
Expand Down
25 changes: 13 additions & 12 deletions src/core/files/file-input-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,30 +88,30 @@ export const FILE_INPUT_POLICIES = {
},
"image-standard": {
id: "image-standard",
accept: "image/*",
description: "Image files up to 12 MB and 24 MP",
accept: ".png,.jpg,.jpeg,.webp,.gif,.avif,image/png,image/jpeg,image/webp,image/gif,image/avif",
description: "Raster image files up to 12 MB and 24 MP. Use SVG tools for SVG files.",
maxBytes: 12 * 1024 * 1024,
maxPixels: 24_000_000,
allowedMimePrefixes: ["image/"],
allowedExtensions: [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif"],
allowedExtensions: [".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif"],
allowedMimeTypes: ["image/png", "image/jpeg", "image/webp", "image/gif", "image/avif"],
},
"image-compact": {
id: "image-compact",
accept: "image/*",
description: "Image files up to 10 MB and 16 MP",
accept: ".png,.jpg,.jpeg,.webp,.gif,.avif,image/png,image/jpeg,image/webp,image/gif,image/avif",
description: "Raster image files up to 10 MB and 16 MP. Use SVG tools for SVG files.",
maxBytes: 10 * 1024 * 1024,
maxPixels: 16_000_000,
allowedMimePrefixes: ["image/"],
allowedExtensions: [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".avif"],
allowedExtensions: [".png", ".jpg", ".jpeg", ".webp", ".gif", ".avif"],
allowedMimeTypes: ["image/png", "image/jpeg", "image/webp", "image/gif", "image/avif"],
},
"image-logo": {
id: "image-logo",
accept: "image/*",
description: "Logo/avatar images up to 2 MB and 4 MP",
accept: ".png,.jpg,.jpeg,.webp,.gif,image/png,image/jpeg,image/webp,image/gif",
description: "Raster logo/avatar images up to 2 MB and 4 MP. Use SVG tools for SVG files.",
maxBytes: 2 * 1024 * 1024,
maxPixels: 4_000_000,
allowedMimePrefixes: ["image/"],
allowedExtensions: [".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg"],
allowedExtensions: [".png", ".jpg", ".jpeg", ".webp", ".gif"],
allowedMimeTypes: ["image/png", "image/jpeg", "image/webp", "image/gif"],
},
svg: {
id: "svg",
Expand Down Expand Up @@ -165,6 +165,7 @@ function hasAllowedFileType(file: File, policy: FileInputPolicy): boolean {
const extension = fileExtension(file)
const hasTypeRules = Boolean(policy.allowedMimePrefixes?.length || policy.allowedMimeTypes?.length || policy.allowedExtensions?.length)
if (!hasTypeRules) return true
if (extension && policy.allowedExtensions?.length && !policy.allowedExtensions.includes(extension)) return false
if (mime && policy.allowedMimePrefixes?.some((prefix) => mime.startsWith(prefix))) return true
if (mime && policy.allowedMimeTypes?.includes(mime)) return true
if (extension && policy.allowedExtensions?.includes(extension)) return true
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -3225,6 +3225,7 @@
"verification_mode_aria_label": "Prüfmodus",
"verification_mode_title": "Prüfmodus",
"verification_mode_description": "Browserinterne Hilfe. Erfasst Anfrage-Hosts und Storage-Schlüssel, aber keine Payloads, Headers, Query-Strings oder Werte.",
"verification_mode_scope": "Umfang: nur fetch, sendBeacon und Storage-Änderungen. Nutze DevTools Network für vom Browser geladene Bilder, Skripte, Downloads und andere Ressourcen.",
"verification_mode_on": "Ein",
"verification_mode_off": "Aus",
"verification_mode_browser_local_tool": "Browser-lokales Tool",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3225,6 +3225,7 @@
"verification_mode_aria_label": "Verification mode",
"verification_mode_title": "Verification mode",
"verification_mode_description": "Browser-only aid. It records request hosts and storage keys, not payloads, headers, query strings, or values.",
"verification_mode_scope": "Scope: fetch, sendBeacon, and storage changes only. Use DevTools Network for browser-loaded images, scripts, downloads, and other resource loads.",
"verification_mode_on": "On",
"verification_mode_off": "Off",
"verification_mode_browser_local_tool": "Browser-local tool",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -3225,6 +3225,7 @@
"verification_mode_aria_label": "Mode de vérification",
"verification_mode_title": "Mode de vérification",
"verification_mode_description": "Aide dans le navigateur. Il enregistre les hôtes de requête et clés de stockage, pas les payloads, headers, chaînes de requête ni valeurs.",
"verification_mode_scope": "Portée : fetch, sendBeacon et changements de stockage seulement. Utilisez DevTools Network pour les images, scripts, téléchargements et autres ressources chargés par le navigateur.",
"verification_mode_on": "Activé",
"verification_mode_off": "Désactivé",
"verification_mode_browser_local_tool": "Outil local au navigateur",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -3225,6 +3225,7 @@
"verification_mode_aria_label": "検証モード",
"verification_mode_title": "検証モード",
"verification_mode_description": "ブラウザー内だけの補助機能です。リクエスト先ホストとストレージキーを記録し、payload、headers、クエリ文字列、値は記録しません。",
"verification_mode_scope": "範囲: fetch、sendBeacon、ストレージ変更のみを対象にします。ブラウザーが読み込む画像、スクリプト、ダウンロード、その他のリソースは DevTools Network で確認してください。",
"verification_mode_on": "オン",
"verification_mode_off": "オフ",
"verification_mode_browser_local_tool": "ブラウザー内ツール",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -3225,6 +3225,7 @@
"verification_mode_aria_label": "확인 모드",
"verification_mode_title": "확인 모드",
"verification_mode_description": "브라우저 안에서만 쓰는 보조 기능입니다. 요청 호스트와 저장소 키를 기록하며 payload, headers, 쿼리 문자열, 값은 기록하지 않습니다.",
"verification_mode_scope": "범위: fetch, sendBeacon, 저장소 변경만 관찰합니다. 브라우저가 로드하는 이미지, 스크립트, 다운로드 및 기타 리소스는 DevTools Network로 확인하세요.",
"verification_mode_on": "켜짐",
"verification_mode_off": "꺼짐",
"verification_mode_browser_local_tool": "브라우저 로컬 도구",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -3225,6 +3225,7 @@
"verification_mode_aria_label": "验证模式",
"verification_mode_title": "验证模式",
"verification_mode_description": "仅在浏览器内辅助验证。它记录请求主机和存储键,不记录 payload、headers、查询字符串或存储值。",
"verification_mode_scope": "范围:仅覆盖 fetch、sendBeacon 和存储变更。浏览器加载的图片、脚本、下载和其他资源请求仍请使用 DevTools Network 确认。",
"verification_mode_on": "开启",
"verification_mode_off": "关闭",
"verification_mode_browser_local_tool": "浏览器本地工具",
Expand Down
1 change: 1 addition & 0 deletions src/core/i18n/translations/zh-TW.json
Original file line number Diff line number Diff line change
Expand Up @@ -3225,6 +3225,7 @@
"verification_mode_aria_label": "驗證模式",
"verification_mode_title": "驗證模式",
"verification_mode_description": "僅在瀏覽器內輔助驗證。它記錄請求主機和儲存鍵,不記錄 payload、headers、查詢字串或儲存值。",
"verification_mode_scope": "範圍:僅涵蓋 fetch、sendBeacon 與儲存變更。瀏覽器載入的圖片、指令碼、下載與其他資源請求仍請使用 DevTools Network 確認。",
"verification_mode_on": "開啟",
"verification_mode_off": "關閉",
"verification_mode_browser_local_tool": "瀏覽器本地工具",
Expand Down
42 changes: 34 additions & 8 deletions src/core/storage/tool-persistence-policy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { removeStorageKey, writeStorageString } from "./tool-persistence"
import { getAnalyticsOptOutStorageKey } from "@/core/analytics/preferences"

export type ToolInputPersistenceMode = true | false | "opt-in"

Expand Down Expand Up @@ -28,24 +29,49 @@ export function enforceToolInputPersistencePolicy(policy: ToolPersistencePolicy,
writeStorageString(policy.inputStorageKey, input)
}

export function clearByteflowLocalData(): number {
export type ClearByteflowBrowserDataOptions = {
preserveAnalyticsOptOut?: boolean
includeSessionStorage?: boolean
}

function collectByteflowStorageKeys(
storage: Storage,
preservedKeys: ReadonlySet<string>,
): string[] {
const keys: string[] = []
for (let index = 0; index < storage.length; index += 1) {
const key = storage.key(index)
if (key?.startsWith("byteflow:") && !preservedKeys.has(key)) keys.push(key)
}
return keys
}

export function clearByteflowBrowserData(options: ClearByteflowBrowserDataOptions = {}): number {
if (typeof window === "undefined") return 0

const preserveAnalyticsOptOut = options.preserveAnalyticsOptOut ?? true
const preservedKeys = new Set<string>()
if (preserveAnalyticsOptOut) preservedKeys.add(getAnalyticsOptOutStorageKey())

let removed = 0
const keys: string[] = []
try {
for (let index = 0; index < window.localStorage.length; index += 1) {
const key = window.localStorage.key(index)
if (key?.startsWith("byteflow:")) keys.push(key)
}

keys.forEach((key) => {
collectByteflowStorageKeys(window.localStorage, preservedKeys).forEach((key) => {
window.localStorage.removeItem(key)
removed += 1
})
if (options.includeSessionStorage ?? true) {
collectByteflowStorageKeys(window.sessionStorage, preservedKeys).forEach((key) => {
window.sessionStorage.removeItem(key)
removed += 1
})
}
} catch {
return removed
}

return removed
}

export function clearByteflowLocalData(): number {
return clearByteflowBrowserData()
}
64 changes: 60 additions & 4 deletions src/core/trust/verification-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,69 @@ export type VerificationStorageEntry = {
timestamp: string
}

export const ALLOWED_STORAGE_KEY_PREFIXES = [
"byteflow:",
"theme",
export type VerificationStorageKeyPolicy = {
exactKeys: readonly string[]
safePrefixes: readonly string[]
reviewSubstrings: readonly string[]
}

export const VERIFICATION_STORAGE_KEY_POLICY: VerificationStorageKeyPolicy = {
exactKeys: [
"theme",
"byteflow:analytics:opt-out",
"byteflow:tools:favorites",
"byteflow:tools:recent",
"byteflow:preferred-locale",
"byteflow:pwa-install:visit-count",
"byteflow:pwa-install:dismissed-until",
"byteflow:pwa-install:session-prompted",
"byteflow:pwa-install:installed",
"byteflow:base64:mode",
"byteflow:base64:operation",
"byteflow:csv-json-converter:direction",
"byteflow:csv-json-converter:delimiter",
"byteflow:csv-json-converter:has-header",
"byteflow:csv-json-converter:type-inference",
"byteflow:json-formatter:view-mode",
"byteflow:jwt-workbench:algorithm",
"byteflow:pipeline-builder:onboarding-dismissed",
"byteflow:url-encode-decode:strategy",
"byteflow:url-encode-decode:operation",
"byteflow:yaml-json-converter:mode",
"byteflow:yaml-json-converter:from-format",
"byteflow:yaml-json-converter:to-format",
],
safePrefixes: [],
reviewSubstrings: [
"input",
"output",
"payload",
"token",
"jwt",
"secret",
"password",
"file",
"blob",
"image",
"log",
"har",
"request",
"response",
"body",
"content",
],
} as const

const REVIEW_STORAGE_KEY_SUBSTRINGS = [
...VERIFICATION_STORAGE_KEY_POLICY.reviewSubstrings,
] as const

export function isAllowedVerificationStorageKey(key: string): boolean {
return ALLOWED_STORAGE_KEY_PREFIXES.some((prefix) => key === prefix || key.startsWith(prefix))
const normalized = key.trim().toLowerCase()
if (!normalized) return false
if (REVIEW_STORAGE_KEY_SUBSTRINGS.some((substring) => normalized.includes(substring))) return false
if (VERIFICATION_STORAGE_KEY_POLICY.exactKeys.some((allowedKey) => allowedKey.toLowerCase() === normalized)) return true
return VERIFICATION_STORAGE_KEY_POLICY.safePrefixes.some((prefix) => normalized.startsWith(prefix.toLowerCase()))
}

export function sanitizeVerificationUrl(input: unknown, baseHref: string) {
Expand Down
7 changes: 5 additions & 2 deletions src/features/privacy/local-data-controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
readFavoriteToolKeys,
readRecentToolKeys,
} from "@/core/storage/tool-discovery-state"
import { clearByteflowLocalData } from "@/core/storage/tool-persistence-policy"
import { clearByteflowBrowserData } from "@/core/storage/tool-persistence-policy"
import { clearSavedRecipes, countSavedRecipes, isRecipeStoreAvailable } from "@/features/pipeline/recipe-store"

type LocalDataSummary = {
Expand Down Expand Up @@ -98,7 +98,10 @@ export function LocalDataControls() {

const clearData = async () => {
setPendingAction("all")
const removed = clearByteflowLocalData()
const removed = clearByteflowBrowserData({
preserveAnalyticsOptOut: true,
includeSessionStorage: true,
})
let recipeCount = 0
if (isRecipeStoreAvailable()) {
const countResult = await countSavedRecipes()
Expand Down
16 changes: 15 additions & 1 deletion src/features/tools/csv-json-converter/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ function serializeCsvCell(value: unknown): string {
return String(value)
}

function isPlainObjectRow(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value)
}

function describeJsonRowShape(value: unknown): string {
if (value === null) return "null"
if (Array.isArray(value)) return "array"
return typeof value
}

export function jsonToCsv(json: string, delimiter: string, includeHeader: boolean): string {
const parsed = JSON.parse(json)
const effectiveDelimiter = delimiter === "auto" ? "," : delimiter
Expand All @@ -136,7 +146,11 @@ export function jsonToCsv(json: string, delimiter: string, includeHeader: boolea

if (parsed.length === 0) return ""

if (typeof parsed[0] === "object" && !Array.isArray(parsed[0])) {
if (parsed.some(isPlainObjectRow)) {
const invalidRowIndex = parsed.findIndex((item) => !isPlainObjectRow(item))
if (invalidRowIndex !== -1) {
throw new Error(`JSON array rows must be objects when converting to header-based CSV. Row ${invalidRowIndex + 1} is ${describeJsonRowShape(parsed[invalidRowIndex])}.`)
}
const flattened = parsed.map((item) => flattenObject(item as Record<string, unknown>))
const allKeys = [...new Set(flattened.flatMap((obj) => Object.keys(obj)))]

Expand Down
24 changes: 15 additions & 9 deletions src/features/tools/jwt-verifier/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ function hashForHmacAlgorithm(algorithm: SupportedHmacJwtAlgorithm): "SHA-256" |
return "SHA-256"
}

export function classifyJwtVerificationAlgorithm(algorithm: string): "hmac" | "unsupported" | "unsigned" {
if (algorithm.toLowerCase() === "none") return "unsigned"
if (isSupportedHmacAlgorithm(algorithm)) return "hmac"
export function normalizeJwtAlgorithm(algorithm: unknown): string {
return typeof algorithm === "string" && algorithm.trim() ? algorithm : "non-string alg"
}

export function classifyJwtVerificationAlgorithm(algorithm: unknown): "hmac" | "unsupported" | "unsigned" {
const normalizedAlgorithm = normalizeJwtAlgorithm(algorithm)
if (normalizedAlgorithm.toLowerCase() === "none") return "unsigned"
if (isSupportedHmacAlgorithm(normalizedAlgorithm)) return "hmac"
return "unsupported"
}

Expand All @@ -81,15 +86,16 @@ async function verifyHmacJwt(token: string, secret: string, algorithm: Supported
return bytesToBase64Url(new Uint8Array(signature)) === parts[2]
}

export async function verifyJwtSignature(token: string, secret: string, algorithm: string): Promise<JwtSignatureVerificationResult | null> {
const classification = classifyJwtVerificationAlgorithm(algorithm)
export async function verifyJwtSignature(token: string, secret: string, algorithm: unknown): Promise<JwtSignatureVerificationResult | null> {
const normalizedAlgorithm = normalizeJwtAlgorithm(algorithm)
const classification = classifyJwtVerificationAlgorithm(normalizedAlgorithm)
if (classification === "unsigned") return { status: "unsigned", algorithm: "none" }
if (classification === "unsupported") return { status: "unsupported", algorithm }
if (classification === "unsupported") return { status: "unsupported", algorithm: normalizedAlgorithm }
if (!secret.trim()) return null
if (!isSupportedHmacAlgorithm(algorithm)) return { status: "unsupported", algorithm }
if (!isSupportedHmacAlgorithm(normalizedAlgorithm)) return { status: "unsupported", algorithm: normalizedAlgorithm }

const valid = await verifyHmacJwt(token, secret, algorithm)
return { status: valid ? "valid" : "invalid", algorithm }
const valid = await verifyHmacJwt(token, secret, normalizedAlgorithm)
return { status: valid ? "valid" : "invalid", algorithm: normalizedAlgorithm }
}

export function parseUnixTimestampClaim(value: unknown): { ok: true; seconds: number; iso: string } | { ok: false; error: string } {
Expand Down
10 changes: 7 additions & 3 deletions src/features/tools/jwt-verifier/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
decodeHeader,
decodePayload,
verifyJwtSignature,
normalizeJwtAlgorithm,
type JwtSignatureVerificationResult,
} from "./logic"

Expand Down Expand Up @@ -63,17 +64,18 @@ export function JwtVerifierPage() {
const [claims, setClaims] = React.useState<{ label: string; status: string; value: string }[]>([])

const verify = async () => {
if (!token.trim()) return
if (!token.trim()) return { status: "failed" as const, message: toolT.token_label }

const h = decodeHeader(token)
const p = decodePayload(token)
setHeader(h)
setPayload(p)
const alg = h?.alg as string || "unknown"
const alg = normalizeJwtAlgorithm(h?.alg)

if (p) setClaims(checkClaims(p, claimLabels))

setVerifyResult(await verifyJwtSignature(token, secret, alg))
return { status: "success" as const }
}
const clearAll = () => {
setToken("")
Expand Down Expand Up @@ -103,8 +105,10 @@ export function JwtVerifierPage() {
id: "verify",
label: toolT.verify_action,
icon: ShieldCheck,
onClick: () => { void verify() },
onClick: verify,
variant: "default",
disabled: !token.trim(),
disabledReason: toolT.token_label,
},
]

Expand Down
Loading