From 67b4309f3ee5145bd15428ae401914d68b9cc4d5 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 07:32:41 -0700 Subject: [PATCH 01/28] test(seo): add safe JSON-LD renderer --- src/core/seo/components/json-ld-script.tsx | 19 ++++++++++++ src/core/seo/components/json-ld.tsx | 8 ++--- src/core/seo/components/site-json-ld.tsx | 8 ++--- .../tool-content-template-modules/core.tsx | 7 ++--- .../components/install-app-client.tsx | 3 +- tests/unit/json-ld-script.test.tsx | 30 +++++++++++++++++++ 6 files changed, 57 insertions(+), 18 deletions(-) create mode 100644 src/core/seo/components/json-ld-script.tsx create mode 100644 tests/unit/json-ld-script.test.tsx diff --git a/src/core/seo/components/json-ld-script.tsx b/src/core/seo/components/json-ld-script.tsx new file mode 100644 index 00000000..659ad9db --- /dev/null +++ b/src/core/seo/components/json-ld-script.tsx @@ -0,0 +1,19 @@ +import type { ScriptHTMLAttributes } from "react" + +type JsonLdScriptProps = Omit, "children" | "dangerouslySetInnerHTML" | "type"> & { + jsonLd: unknown +} + +export function serializeJsonLd(jsonLd: unknown): string { + return JSON.stringify(jsonLd).replace(/ + ) +} diff --git a/src/core/seo/components/json-ld.tsx b/src/core/seo/components/json-ld.tsx index ebe7f176..82712ee9 100644 --- a/src/core/seo/components/json-ld.tsx +++ b/src/core/seo/components/json-ld.tsx @@ -1,6 +1,7 @@ import { buildBreadcrumbJsonLd } from "@/core/seo/seo"; import { getToolBySlug, type ToolMeta } from "@/core/registry"; import type { Locale } from "@/core/i18n/i18n"; +import { JsonLdScript } from "./json-ld-script"; /** * Renders BreadcrumbList JSON-LD structured data for a tool page. @@ -12,12 +13,7 @@ export function ToolBreadcrumbJsonLd({ lang, slug }: { lang: Locale; slug: strin const jsonLd = buildBreadcrumbJsonLd({ lang, tool }); - return ( - ", + text: "你好", + }) + + expect(serialized).toContain("\\u003c/script>") + expect(serialized).toContain("\\u003cimg") + expect(serialized).not.toContain("") + expect(JSON.parse(serialized)).toEqual({ + name: "", + text: "你好", + }) + }) + + it("renders application/ld+json scripts with passthrough attributes", () => { + const html = renderToStaticMarkup( + , + ) + + expect(html).toContain('type="application/ld+json"') + expect(html).toContain('data-faq-schema="tool"') + expect(html).toContain('"@type":"FAQPage"') + }) +}) From aaf4cdd8b4df6109f32edb2aa367c056b951e57a Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 07:35:10 -0700 Subject: [PATCH 02/28] feat(routing): make tool handoff sessionStorage-first --- src/core/routing/tool-handoff.ts | 11 +++++------ tests/unit/tool-handoff.test.ts | 27 +++++++++++++++++++-------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/core/routing/tool-handoff.ts b/src/core/routing/tool-handoff.ts index e3e232a6..5cd8cf11 100644 --- a/src/core/routing/tool-handoff.ts +++ b/src/core/routing/tool-handoff.ts @@ -1,7 +1,6 @@ const HANDOFF_PARAM = "handoff" const HANDOFF_REF_PARAM = "handoff_ref" const HANDOFF_STORAGE_PREFIX = "byteflow:handoff:" -const HANDOFF_QUERY_MAX_CHARS = 3800 const STORAGE_PROBE_KEY = `${HANDOFF_STORAGE_PREFIX}probe` let sessionStorageAvailable: boolean | null = null @@ -50,7 +49,7 @@ function fromBase64Url(value: string): string | null { } } -export function buildToolHandoffHref(lang: string, slug: string, payload: string): string { +export function buildShareableToolHandoffHref(lang: string, slug: string, payload: string): string { const basePath = buildBasePath(lang, slug) const text = payload.trim() if (!text) return basePath @@ -59,6 +58,8 @@ export function buildToolHandoffHref(lang: string, slug: string, payload: string return `${basePath}?${HANDOFF_PARAM}=${encodeURIComponent(encoded)}` } +export const buildToolHandoffHref = buildShareableToolHandoffHref + function canUseSessionStorage(): boolean { if (sessionStorageAvailable !== null) return sessionStorageAvailable if (typeof window === "undefined") { @@ -113,11 +114,9 @@ export function buildToolHandoffLink(lang: string, slug: string, payload: string } } - const encodedPayload = encodeURIComponent(toBase64Url(text)) - const queryHref = `${basePath}?${HANDOFF_PARAM}=${encodedPayload}` - if (encodedPayload.length <= HANDOFF_QUERY_MAX_CHARS || !canUseSessionStorage()) { + if (!canUseSessionStorage()) { return { - href: queryHref, + href: buildShareableToolHandoffHref(lang, slug, text), prime: () => undefined, } } diff --git a/tests/unit/tool-handoff.test.ts b/tests/unit/tool-handoff.test.ts index 7ad88fc1..b4d72deb 100644 --- a/tests/unit/tool-handoff.test.ts +++ b/tests/unit/tool-handoff.test.ts @@ -1,19 +1,24 @@ import { describe, expect, it } from "vitest" -import { buildToolHandoffHref, buildToolHandoffLink, getToolHandoffFromSearchParams } from "@/core/routing/tool-handoff" +import { + buildShareableToolHandoffHref, + buildToolHandoffHref, + buildToolHandoffLink, + getToolHandoffFromSearchParams, +} from "@/core/routing/tool-handoff" describe("tool handoff", () => { - it("builds a localized handoff URL", () => { - const href = buildToolHandoffHref("en", "json-to-typescript", '{"a":1}') + it("builds a localized shareable handoff URL", () => { + const href = buildShareableToolHandoffHref("en", "json-to-typescript", '{"a":1}') expect(href.startsWith("/en/json-to-typescript?handoff=")).toBe(true) }) it("returns base path when payload is empty", () => { - expect(buildToolHandoffHref("zh-CN", "javascript-minifier", " ")).toBe("/zh-CN/javascript-minifier") + expect(buildShareableToolHandoffHref("zh-CN", "javascript-minifier", " ")).toBe("/zh-CN/javascript-minifier") }) it("round-trips unicode payload from query params", () => { const payload = "你好, JSON\n{\"name\":\"ByteFlow\"}" - const href = buildToolHandoffHref("en", "json-to-typescript", payload) + const href = buildShareableToolHandoffHref("en", "json-to-typescript", payload) const query = href.split("?")[1] || "" const params = new URLSearchParams(query) expect(getToolHandoffFromSearchParams(params)).toBe(payload) @@ -25,16 +30,22 @@ describe("tool handoff", () => { }) it("fails fast when locale is missing instead of defaulting to English", () => { - expect(() => buildToolHandoffHref(" ", "json-to-typescript", '{"a":1}')).toThrow( + expect(() => buildShareableToolHandoffHref(" ", "json-to-typescript", '{"a":1}')).toThrow( "[i18n] tool handoff requires an explicit locale", ) }) - it("falls back to sessionStorage handoff for large payloads", () => { - const payload = "a".repeat(5000) + it("keeps the legacy handoff href export as explicit share-link behavior", () => { + const href = buildToolHandoffHref("en", "json-to-typescript", '{"a":1}') + expect(href.startsWith("/en/json-to-typescript?handoff=")).toBe(true) + }) + + it("uses sessionStorage handoff by default when available", () => { + const payload = '{"token":"secret"}' const handoff = buildToolHandoffLink("en", "json-to-typescript", payload) expect(handoff.href.startsWith("/en/json-to-typescript?handoff_ref=")).toBe(true) + expect(handoff.href).not.toContain("secret") handoff.prime() const query = handoff.href.split("?")[1] || "" From 282b8985512ea5fff9c3dd623d1f5d43b6eea320 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 07:44:11 -0700 Subject: [PATCH 03/28] test(security): require network access metadata --- .../generators/generate-client-tool-lookup.js | 2 + scripts/generators/generate-tool-index.js | 1 + scripts/lib/tool-manifest-lib.js | 7 + src/core/registry/types.ts | 4 + .../instagram-photo-downloader/manifest.ts | 1 + .../tools/vimeo-thumbnail-grabber/manifest.ts | 1 + .../youtube-thumbnail-grabber/manifest.ts | 1 + src/generated/client-tool-lookup.ts | 324 ++++++++++++------ src/generated/tool-index.json | 133 ++++++- .../tool-network-access-metadata.test.ts | 74 ++++ 10 files changed, 445 insertions(+), 103 deletions(-) create mode 100644 tests/guards/tool-network-access-metadata.test.ts diff --git a/scripts/generators/generate-client-tool-lookup.js b/scripts/generators/generate-client-tool-lookup.js index 0c64435a..0b835383 100644 --- a/scripts/generators/generate-client-tool-lookup.js +++ b/scripts/generators/generate-client-tool-lookup.js @@ -66,6 +66,7 @@ function buildClientLookupSource() { keywords: tool.keywords, aliases: aliasesByKey.get(tool.key) || [], relatedToolKeys: tool.relatedTools, + networkAccess: tool.networkAccess || "none", } if (tool.searchKeywords) { entry.searchKeywords = tool.searchKeywords @@ -96,6 +97,7 @@ export type ClientToolLookupEntry = { keywords: readonly string[] aliases: readonly string[] relatedToolKeys: readonly string[] + networkAccess: "none" | "user_requested" | "third_party_api" searchKeywords?: readonly string[] } diff --git a/scripts/generators/generate-tool-index.js b/scripts/generators/generate-tool-index.js index a38aa9e7..876e9a46 100644 --- a/scripts/generators/generate-tool-index.js +++ b/scripts/generators/generate-tool-index.js @@ -168,6 +168,7 @@ function buildIndexData() { category: tool.category, relatedTools: tool.relatedTools, keywords: tool.keywords, + networkAccess: tool.networkAccess || "none", updatedAt: tool.updatedAt || null, sourceFile: tool.sourceFile, } diff --git a/scripts/lib/tool-manifest-lib.js b/scripts/lib/tool-manifest-lib.js index b4c00ed1..4fcf9ecf 100644 --- a/scripts/lib/tool-manifest-lib.js +++ b/scripts/lib/tool-manifest-lib.js @@ -6,6 +6,7 @@ export const FEATURE_TOOLS_DIR = path.join(ROOT_DIR, "src/features/tools") export const TOOL_MANIFESTS_PATH = path.join(ROOT_DIR, "src/core/registry/manifests.ts") const REQUIRED_FIELDS = ["key", "slug", "category", "relatedTools", "keywords"] +const NETWORK_ACCESS_VALUES = new Set(["none", "user_requested", "third_party_api"]) function relative(filePath) { return path.relative(ROOT_DIR, filePath).replace(/\\/g, "/") @@ -236,8 +237,13 @@ export function parseToolManifestFile(manifestPath) { const relatedTools = arrayField(body, "relatedTools", manifestPath, true) const searchKeywords = arrayField(body, "searchKeywords", manifestPath) const updatedAt = stringField(body, "updatedAt", manifestPath) + const networkAccess = stringField(body, "networkAccess", manifestPath) const deprecated = parseDeprecated(body, manifestPath) + if (networkAccess && !NETWORK_ACCESS_VALUES.has(networkAccess)) { + throw manifestError(manifestPath, "networkAccess", "must be one of none, user_requested, or third_party_api") + } + const manifest = { key, slug, @@ -249,6 +255,7 @@ export function parseToolManifestFile(manifestPath) { if (updatedAt) manifest.updatedAt = updatedAt if (searchKeywords.length > 0) manifest.searchKeywords = searchKeywords + if (networkAccess) manifest.networkAccess = networkAccess if (deprecated) manifest.deprecated = deprecated return manifest diff --git a/src/core/registry/types.ts b/src/core/registry/types.ts index d9e00581..bc89f9b1 100644 --- a/src/core/registry/types.ts +++ b/src/core/registry/types.ts @@ -2,6 +2,8 @@ import type { ToolCategory } from "./categories" export type { ToolCategory } from "./categories" +export type ToolNetworkAccess = "none" | "user_requested" | "third_party_api" + /** * Tool metadata used by registry, sitemap, SEO, breadcrumbs, and related tools. */ @@ -20,6 +22,8 @@ export interface ToolMeta { updatedAt?: string /** Optional search keywords for use-case and multilingual matching in command palette */ searchKeywords?: string[] + /** Browser network behavior used by privacy UI and CI guards */ + networkAccess?: ToolNetworkAccess /** Optional deprecation metadata - marks tool as deprecated with message and alternatives */ deprecated?: { /** Translation key for deprecation message (optional, falls back to generic message) */ diff --git a/src/features/tools/instagram-photo-downloader/manifest.ts b/src/features/tools/instagram-photo-downloader/manifest.ts index 7682066d..a4710caa 100644 --- a/src/features/tools/instagram-photo-downloader/manifest.ts +++ b/src/features/tools/instagram-photo-downloader/manifest.ts @@ -6,6 +6,7 @@ export const toolManifest = { category: "generators", relatedTools: ["instagram_post_generator", "instagram_story_generator", "instagram_filters", "image_resizer"], keywords: ["instagram photo downloader", "download instagram image", "instagram media saver", "authorized ig image download"], + networkAccess: "user_requested", deprecated: { reason: "strategic-refocus", }, diff --git a/src/features/tools/vimeo-thumbnail-grabber/manifest.ts b/src/features/tools/vimeo-thumbnail-grabber/manifest.ts index 3c09dfa1..5a7d4212 100644 --- a/src/features/tools/vimeo-thumbnail-grabber/manifest.ts +++ b/src/features/tools/vimeo-thumbnail-grabber/manifest.ts @@ -6,6 +6,7 @@ export const toolManifest = { category: "generators", relatedTools: ["youtube_thumbnail_grabber", "tweet_to_image_converter", "open_graph_meta_generator", "image_resizer"], keywords: ["vimeo thumbnail grabber", "vimeo thumbnail downloader", "vimeo cover image", "get vimeo video thumbnail"], + networkAccess: "user_requested", deprecated: { alternatives: ["image_resizer"], reason: "strategic-refocus", diff --git a/src/features/tools/youtube-thumbnail-grabber/manifest.ts b/src/features/tools/youtube-thumbnail-grabber/manifest.ts index 160cb66f..f5430f70 100644 --- a/src/features/tools/youtube-thumbnail-grabber/manifest.ts +++ b/src/features/tools/youtube-thumbnail-grabber/manifest.ts @@ -6,6 +6,7 @@ export const toolManifest = { category: "generators", relatedTools: ["vimeo_thumbnail_grabber", "tweet_to_image_converter", "open_graph_meta_generator", "image_resizer"], keywords: ["youtube thumbnail grabber", "youtube thumbnail downloader", "youtube video thumbnail", "get youtube thumbnail url"], + networkAccess: "user_requested", deprecated: { alternatives: ["image_resizer"], reason: "strategic-refocus", diff --git a/src/generated/client-tool-lookup.ts b/src/generated/client-tool-lookup.ts index 77a47bc3..f970fc52 100644 --- a/src/generated/client-tool-lookup.ts +++ b/src/generated/client-tool-lookup.ts @@ -9,6 +9,7 @@ export type ClientToolLookupEntry = { keywords: readonly string[] aliases: readonly string[] relatedToolKeys: readonly string[] + networkAccess: "none" | "user_requested" | "third_party_api" searchKeywords?: readonly string[] } @@ -36,6 +37,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_to_typescript", "yaml_json_converter" ], + "networkAccess": "none", "searchKeywords": [ "beautify", "pretty print", @@ -65,6 +67,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "yaml_json_converter", "sql_formatter" ], + "networkAccess": "none", "searchKeywords": [ "beautify xml", "format xml", @@ -90,6 +93,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "xml_formatter", "html_css_beautifier" ], + "networkAccess": "none", "searchKeywords": [ "beautify sql", "format sql", @@ -116,6 +120,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "sql_formatter", "markdown_preview" ], + "networkAccess": "none", "searchKeywords": [ "beautify js", "format javascript", @@ -141,7 +146,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_minifier", "html_formatter", "json_formatter" - ] + ], + "networkAccess": "none" }, "html_minifier": { "key": "html_minifier", @@ -158,7 +164,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "html_css_beautifier", "css_minifier", "javascript_minifier" - ] + ], + "networkAccess": "none" }, "html_encoder_decoder": { "key": "html_encoder_decoder", @@ -175,7 +182,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "html_minifier", "url_encode_decode", "base64_encode_decode" - ] + ], + "networkAccess": "none" }, "html_css_beautifier": { "key": "html_css_beautifier", @@ -192,7 +200,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_minifier", "svg_optimizer", "xml_formatter" - ] + ], + "networkAccess": "none" }, "html_formatter": { "key": "html_formatter", @@ -210,6 +219,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_minifier", "markdown_preview" ], + "networkAccess": "none", "searchKeywords": [ "beautify html", "format html", @@ -236,6 +246,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "jsonpath_playground", "xml_formatter" ], + "networkAccess": "none", "searchKeywords": [ "convert yaml", "yaml parser", @@ -262,6 +273,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "text_diff_checker", "lorem_ipsum" ], + "networkAccess": "none", "searchKeywords": [ "preview markdown", "markdown viewer", @@ -287,7 +299,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "html_formatter", "html_encoder_decoder", "text_diff_checker" - ] + ], + "networkAccess": "none" }, "json_to_typescript": { "key": "json_to_typescript", @@ -304,6 +317,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "yaml_json_converter", "jsonpath_playground" ], + "networkAccess": "none", "searchKeywords": [ "generate types", "interface", @@ -328,7 +342,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "html_css_beautifier", "svg_optimizer", "json_formatter" - ] + ], + "networkAccess": "none" }, "svg_optimizer": { "key": "svg_optimizer", @@ -344,7 +359,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_minifier", "html_css_beautifier", "image_base64" - ] + ], + "networkAccess": "none" }, "jsonpath_playground": { "key": "jsonpath_playground", @@ -361,7 +377,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_diff_viewer", "json_to_typescript", "yaml_json_converter" - ] + ], + "networkAccess": "none" }, "openapi_viewer": { "key": "openapi_viewer", @@ -377,7 +394,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_formatter", "yaml_json_converter", "jsonpath_playground" - ] + ], + "networkAccess": "none" }, "json_diff_viewer": { "key": "json_diff_viewer", @@ -393,7 +411,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_formatter", "text_diff_checker", "jsonpath_playground" - ] + ], + "networkAccess": "none" }, "csv_json_converter": { "key": "csv_json_converter", @@ -410,7 +429,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "yaml_json_converter", "json_to_typescript", "text_diff_checker" - ] + ], + "networkAccess": "none" }, "base64_encode_decode": { "key": "base64_encode_decode", @@ -427,6 +447,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "hash_generator", "image_base64" ], + "networkAccess": "none", "searchKeywords": [ "encode base64", "decode base64", @@ -458,6 +479,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "jwt_decoder", "user_agent_parser" ], + "networkAccess": "none", "searchKeywords": [ "encode url", "decode url", @@ -488,6 +510,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "hash_generator", "url_encode_decode" ], + "networkAccess": "none", "searchKeywords": [ "decode token", "parse jwt", @@ -514,7 +537,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "jwt_verifier", "base64_encode_decode", "hash_generator" - ] + ], + "networkAccess": "none" }, "hash_generator": { "key": "hash_generator", @@ -532,6 +556,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "jwt_decoder", "password_generator" ], + "networkAccess": "none", "searchKeywords": [ "generate hash", "checksum", @@ -558,7 +583,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "base64_encode_decode", "text_diff_checker", "password_generator" - ] + ], + "networkAccess": "none" }, "text_diff_checker": { "key": "text_diff_checker", @@ -574,7 +600,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_diff_viewer", "markdown_preview", "base64_encode_decode" - ] + ], + "networkAccess": "none" }, "multiple_whitespace_remover": { "key": "multiple_whitespace_remover", @@ -591,7 +618,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "slugify_case_converter", "markdown_preview", "lorem_ipsum" - ] + ], + "networkAccess": "none" }, "letter_counter": { "key": "letter_counter", @@ -608,7 +636,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "text_diff_checker", "lorem_ipsum", "slugify_case_converter" - ] + ], + "networkAccess": "none" }, "bionic_reading_converter": { "key": "bionic_reading_converter", @@ -625,7 +654,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "multiple_whitespace_remover", "markdown_preview", "text_diff_checker" - ] + ], + "networkAccess": "none" }, "google_fonts_pair_finder": { "key": "google_fonts_pair_finder", @@ -642,7 +672,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "ai_color_palette_generator", "code_to_image_converter", "open_graph_meta_generator" - ] + ], + "networkAccess": "none" }, "text_to_handwriting_converter": { "key": "text_to_handwriting_converter", @@ -659,7 +690,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "google_fonts_pair_finder", "image_caption_generator", "instagram_post_generator" - ] + ], + "networkAccess": "none" }, "code_to_image_converter": { "key": "code_to_image_converter", @@ -676,7 +708,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "ascii_art_generator", "json_formatter", "html_formatter" - ] + ], + "networkAccess": "none" }, "image_base64": { "key": "image_base64", @@ -691,7 +724,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "base64_encode_decode", "svg_optimizer", "qr_code_generator" - ] + ], + "networkAccess": "none" }, "unix_timestamp": { "key": "unix_timestamp", @@ -707,7 +741,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "uuid_generator", "cron_visualizer", "crontab_generator" - ] + ], + "networkAccess": "none" }, "uuid_generator": { "key": "uuid_generator", @@ -724,6 +759,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "unix_timestamp", "lorem_ipsum" ], + "networkAccess": "none", "searchKeywords": [ "generate uuid", "unique id", @@ -748,7 +784,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "password_generator", "uuid_generator", "markdown_preview" - ] + ], + "networkAccess": "none" }, "password_generator": { "key": "password_generator", @@ -763,7 +800,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "hash_generator", "uuid_generator", "lorem_ipsum" - ] + ], + "networkAccess": "none" }, "color_converter": { "key": "color_converter", @@ -779,7 +817,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_minifier", "html_css_beautifier", "svg_optimizer" - ] + ], + "networkAccess": "none" }, "react_native_shadow_generator": { "key": "react_native_shadow_generator", @@ -796,7 +835,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_glassmorphism_generator", "css_border_radius_generator", "color_converter" - ] + ], + "networkAccess": "none" }, "ai_color_palette_generator": { "key": "ai_color_palette_generator", @@ -813,7 +853,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_shades_generator", "color_converter", "css_gradient_generator" - ] + ], + "networkAccess": "none" }, "color_mixer": { "key": "color_mixer", @@ -830,7 +871,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_converter", "ai_color_palette_generator", "css_gradient_generator" - ] + ], + "networkAccess": "none" }, "color_shades_generator": { "key": "color_shades_generator", @@ -847,7 +889,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_mixer", "color_converter", "css_gradient_generator" - ] + ], + "networkAccess": "none" }, "image_average_color_finder": { "key": "image_average_color_finder", @@ -864,7 +907,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "ai_color_palette_generator", "color_shades_generator", "color_converter" - ] + ], + "networkAccess": "none" }, "image_caption_generator": { "key": "image_caption_generator", @@ -881,7 +925,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "code_to_image_converter", "image_average_color_finder", "color_mixer" - ] + ], + "networkAccess": "none" }, "image_color_extractor": { "key": "image_color_extractor", @@ -898,7 +943,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_shades_generator", "ai_color_palette_generator", "color_mixer" - ] + ], + "networkAccess": "none" }, "image_color_picker": { "key": "image_color_picker", @@ -915,7 +961,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_average_color_finder", "color_converter", "color_mixer" - ] + ], + "networkAccess": "none" }, "image_cropper": { "key": "image_cropper", @@ -932,7 +979,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_caption_generator", "code_to_image_converter", "image_base64" - ] + ], + "networkAccess": "none" }, "image_filters": { "key": "image_filters", @@ -949,7 +997,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_cropper", "image_caption_generator", "image_color_extractor" - ] + ], + "networkAccess": "none" }, "instagram_filters": { "key": "instagram_filters", @@ -966,7 +1015,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "instagram_story_generator", "image_filters", "instagram_photo_downloader" - ] + ], + "networkAccess": "none" }, "instagram_post_generator": { "key": "instagram_post_generator", @@ -983,7 +1033,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "tweet_generator", "instagram_filters", "open_graph_meta_generator" - ] + ], + "networkAccess": "none" }, "instagram_story_generator": { "key": "instagram_story_generator", @@ -1000,7 +1051,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "tweet_generator", "instagram_filters", "open_graph_meta_generator" - ] + ], + "networkAccess": "none" }, "open_graph_meta_generator": { "key": "open_graph_meta_generator", @@ -1017,7 +1069,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "tweet_to_image_converter", "instagram_post_generator", "instagram_story_generator" - ] + ], + "networkAccess": "none" }, "tweet_generator": { "key": "tweet_generator", @@ -1034,7 +1087,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "open_graph_meta_generator", "twitter_ad_revenue_generator", "instagram_post_generator" - ] + ], + "networkAccess": "none" }, "tweet_to_image_converter": { "key": "tweet_to_image_converter", @@ -1051,7 +1105,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "open_graph_meta_generator", "instagram_post_generator", "code_to_image_converter" - ] + ], + "networkAccess": "none" }, "twitter_ad_revenue_generator": { "key": "twitter_ad_revenue_generator", @@ -1068,7 +1123,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "tweet_to_image_converter", "open_graph_meta_generator", "id_generator" - ] + ], + "networkAccess": "none" }, "instagram_photo_downloader": { "key": "instagram_photo_downloader", @@ -1085,7 +1141,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "instagram_story_generator", "instagram_filters", "image_resizer" - ] + ], + "networkAccess": "user_requested" }, "vimeo_thumbnail_grabber": { "key": "vimeo_thumbnail_grabber", @@ -1102,7 +1159,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "tweet_to_image_converter", "open_graph_meta_generator", "image_resizer" - ] + ], + "networkAccess": "user_requested" }, "youtube_thumbnail_grabber": { "key": "youtube_thumbnail_grabber", @@ -1119,7 +1177,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "tweet_to_image_converter", "open_graph_meta_generator", "image_resizer" - ] + ], + "networkAccess": "user_requested" }, "image_resizer": { "key": "image_resizer", @@ -1136,7 +1195,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_filters", "image_base64", "code_to_image_converter" - ] + ], + "networkAccess": "none" }, "photo_censor": { "key": "photo_censor", @@ -1153,7 +1213,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_cropper", "image_resizer", "scanned_pdf_converter" - ] + ], + "networkAccess": "none" }, "scanned_pdf_converter": { "key": "scanned_pdf_converter", @@ -1170,7 +1231,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_filters", "image_resizer", "image_base64" - ] + ], + "networkAccess": "none" }, "svg_blob_generator": { "key": "svg_blob_generator", @@ -1187,7 +1249,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "svg_stroke_to_fill_converter", "svg_optimizer", "css_clip_path_generator" - ] + ], + "networkAccess": "none" }, "svg_pattern_generator": { "key": "svg_pattern_generator", @@ -1204,7 +1267,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "svg_optimizer", "css_background_pattern_generator", "css_gradient_generator" - ] + ], + "networkAccess": "none" }, "svg_stroke_to_fill_converter": { "key": "svg_stroke_to_fill_converter", @@ -1221,7 +1285,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "svg_blob_generator", "svg_pattern_generator", "svg_to_png_converter" - ] + ], + "networkAccess": "none" }, "svg_to_png_converter": { "key": "svg_to_png_converter", @@ -1238,7 +1303,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "svg_pattern_generator", "svg_blob_generator", "svg_stroke_to_fill_converter" - ] + ], + "networkAccess": "none" }, "css_background_pattern_generator": { "key": "css_background_pattern_generator", @@ -1255,7 +1321,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_box_shadow_generator", "color_converter", "css_minifier" - ] + ], + "networkAccess": "none" }, "css_border_radius_generator": { "key": "css_border_radius_generator", @@ -1272,7 +1339,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_box_shadow_generator", "color_converter", "css_minifier" - ] + ], + "networkAccess": "none" }, "css_box_shadow_generator": { "key": "css_box_shadow_generator", @@ -1289,7 +1357,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_background_pattern_generator", "color_converter", "css_minifier" - ] + ], + "networkAccess": "none" }, "css_checkbox_generator": { "key": "css_checkbox_generator", @@ -1306,7 +1375,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_box_shadow_generator", "css_background_pattern_generator", "color_converter" - ] + ], + "networkAccess": "none" }, "css_clip_path_generator": { "key": "css_clip_path_generator", @@ -1323,7 +1393,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_box_shadow_generator", "color_converter", "svg_optimizer" - ] + ], + "networkAccess": "none" }, "css_cubic_bezier_generator": { "key": "css_cubic_bezier_generator", @@ -1340,7 +1411,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_clip_path_generator", "css_checkbox_generator", "css_minifier" - ] + ], + "networkAccess": "none" }, "css_glassmorphism_generator": { "key": "css_glassmorphism_generator", @@ -1357,7 +1429,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_box_shadow_generator", "css_border_radius_generator", "color_converter" - ] + ], + "networkAccess": "none" }, "css_gradient_generator": { "key": "css_gradient_generator", @@ -1374,7 +1447,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_converter", "css_background_pattern_generator", "css_minifier" - ] + ], + "networkAccess": "none" }, "css_loader_generator": { "key": "css_loader_generator", @@ -1391,7 +1465,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_gradient_generator", "css_box_shadow_generator", "css_minifier" - ] + ], + "networkAccess": "none" }, "css_switch_generator": { "key": "css_switch_generator", @@ -1408,7 +1483,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_border_radius_generator", "css_glassmorphism_generator", "color_converter" - ] + ], + "networkAccess": "none" }, "css_text_glitch_effect_generator": { "key": "css_text_glitch_effect_generator", @@ -1425,7 +1501,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_loader_generator", "code_to_image_converter", "css_minifier" - ] + ], + "networkAccess": "none" }, "css_triangle_generator": { "key": "css_triangle_generator", @@ -1442,7 +1519,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_switch_generator", "css_border_radius_generator", "css_minifier" - ] + ], + "networkAccess": "none" }, "qr_code_generator": { "key": "qr_code_generator", @@ -1457,7 +1535,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "uuid_generator", "password_generator", "url_encode_decode" - ] + ], + "networkAccess": "none" }, "barcode_generator": { "key": "barcode_generator", @@ -1474,7 +1553,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "id_generator", "fake_iban_generator", "list_randomizer" - ] + ], + "networkAccess": "none" }, "fake_iban_generator": { "key": "fake_iban_generator", @@ -1491,7 +1571,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "list_randomizer", "id_generator", "password_generator" - ] + ], + "networkAccess": "none" }, "list_randomizer": { "key": "list_randomizer", @@ -1508,7 +1589,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "id_generator", "lorem_ipsum", "password_generator" - ] + ], + "networkAccess": "none" }, "ascii_art_generator": { "key": "ascii_art_generator", @@ -1523,7 +1605,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "lorem_ipsum", "markdown_preview", "text_diff_checker" - ] + ], + "networkAccess": "none" }, "env_parser": { "key": "env_parser", @@ -1539,7 +1622,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_formatter", "yaml_json_converter", "base64_encode_decode" - ] + ], + "networkAccess": "none" }, "id_generator": { "key": "id_generator", @@ -1556,7 +1640,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "password_generator", "unix_timestamp", "hash_generator" - ] + ], + "networkAccess": "none" }, "regex_tester": { "key": "regex_tester", @@ -1573,6 +1658,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "text_diff_checker", "url_encode_decode" ], + "networkAccess": "none", "searchKeywords": [ "regex", "regexp", @@ -1602,7 +1688,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "text_diff_checker", "jsonpath_playground", "url_encode_decode" - ] + ], + "networkAccess": "none" }, "crontab_generator": { "key": "crontab_generator", @@ -1618,6 +1705,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "unix_timestamp", "regex_tester" ], + "networkAccess": "none", "searchKeywords": [ "cron", "crontab", @@ -1648,7 +1736,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_status_codes", "url_parser", "chmod_calculator" - ] + ], + "networkAccess": "none" }, "cron_visualizer": { "key": "cron_visualizer", @@ -1663,7 +1752,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "crontab_generator", "unix_timestamp", "regex_tester" - ] + ], + "networkAccess": "none" }, "http_status_codes": { "key": "http_status_codes", @@ -1679,7 +1769,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_encode_decode", "url_parser", "http_request_builder" - ] + ], + "networkAccess": "none" }, "chmod_calculator": { "key": "chmod_calculator", @@ -1695,7 +1786,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "regex_tester", "user_agent_parser", "cidr_subnet_calculator" - ] + ], + "networkAccess": "none" }, "cidr_subnet_calculator": { "key": "cidr_subnet_calculator", @@ -1712,7 +1804,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_request_builder", "chmod_calculator", "http_status_codes" - ] + ], + "networkAccess": "none" }, "url_parser": { "key": "url_parser", @@ -1729,7 +1822,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_status_codes", "user_agent_parser", "http_request_builder" - ] + ], + "networkAccess": "none" }, "certificate_decoder": { "key": "certificate_decoder", @@ -1746,7 +1840,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "base64_encode_decode", "http_status_codes", "security_header_analyzer" - ] + ], + "networkAccess": "none" }, "http_request_builder": { "key": "http_request_builder", @@ -1763,7 +1858,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_status_codes", "url_encode_decode", "user_agent_parser" - ] + ], + "networkAccess": "none" }, "curl_to_code": { "key": "curl_to_code", @@ -1780,7 +1876,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_parser", "http_status_codes", "jwt_decoder" - ] + ], + "networkAccess": "none" }, "ndjson_formatter": { "key": "ndjson_formatter", @@ -1797,7 +1894,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "csv_json_converter", "json_diff_viewer", "jsonpath_playground" - ] + ], + "networkAccess": "none" }, "jwt_verifier": { "key": "jwt_verifier", @@ -1815,7 +1913,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "hash_generator", "base64_encode_decode", "certificate_decoder" - ] + ], + "networkAccess": "none" }, "slugify_case_converter": { "key": "slugify_case_converter", @@ -1832,7 +1931,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "base64_encode_decode", "hash_generator", "text_diff_checker" - ] + ], + "networkAccess": "none" }, "invisible_chars_detector": { "key": "invisible_chars_detector", @@ -1851,6 +1951,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "base64_encode_decode", "url_encode_decode" ], + "networkAccess": "none", "searchKeywords": [ "invisible chars", "zero width", @@ -1878,7 +1979,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_status_codes", "url_parser", "csp_parser" - ] + ], + "networkAccess": "none" }, "csp_parser": { "key": "csp_parser", @@ -1895,7 +1997,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "certificate_decoder", "header_diff", "robots_txt_tester" - ] + ], + "networkAccess": "none" }, "csv_diff": { "key": "csv_diff", @@ -1912,7 +2015,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "text_diff_checker", "json_diff_viewer", "header_diff" - ] + ], + "networkAccess": "none" }, "header_diff": { "key": "header_diff", @@ -1929,7 +2033,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_request_builder", "csp_parser", "text_diff_checker" - ] + ], + "networkAccess": "none" }, "security_header_analyzer": { "key": "security_header_analyzer", @@ -1946,7 +2051,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "header_diff", "http_status_codes", "certificate_decoder" - ] + ], + "networkAccess": "none" }, "totp_generator": { "key": "totp_generator", @@ -1963,7 +2069,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "hash_generator", "uuid_generator", "id_generator" - ] + ], + "networkAccess": "none" }, "openapi_mock": { "key": "openapi_mock", @@ -1980,7 +2087,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_request_builder", "json_formatter", "curl_to_code" - ] + ], + "networkAccess": "none" }, "docker_run_to_compose": { "key": "docker_run_to_compose", @@ -1998,6 +2106,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "chmod_calculator", "curl_to_code" ], + "networkAccess": "none", "searchKeywords": [ "docker", "compose", @@ -2026,6 +2135,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "regex_tester", "text_diff_checker" ], + "networkAccess": "none", "searchKeywords": [ "parse logs", "analyze logs", @@ -2053,6 +2163,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_to_typescript", "yaml_json_converter" ], + "networkAccess": "none", "searchKeywords": [ "jq", "json query", @@ -2079,7 +2190,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "security_header_analyzer", "jwt_decoder", "text_diff_checker" - ] + ], + "networkAccess": "none" }, "gzip_brotli_lab": { "key": "gzip_brotli_lab", @@ -2096,7 +2208,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_encode_decode", "http_request_builder", "local_log_parser" - ] + ], + "networkAccess": "none" }, "yaml_merge_patch_explorer": { "key": "yaml_merge_patch_explorer", @@ -2113,7 +2226,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_diff_viewer", "openapi_viewer", "docker_run_to_compose" - ] + ], + "networkAccess": "none" }, "yq_playground": { "key": "yq_playground", @@ -2131,7 +2245,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_formatter", "jq_playground", "jsonpath_playground" - ] + ], + "networkAccess": "none" }, "structured_data_visualizer": { "key": "structured_data_visualizer", @@ -2149,7 +2264,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "yaml_json_converter", "xml_formatter", "jsonpath_playground" - ] + ], + "networkAccess": "none" }, "har_viewer_sanitizer": { "key": "har_viewer_sanitizer", @@ -2167,7 +2283,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "log_scrubber", "http_status_codes", "security_header_analyzer" - ] + ], + "networkAccess": "none" }, "pipeline_builder": { "key": "pipeline_builder", @@ -2186,6 +2303,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_encode_decode", "log_scrubber" ], + "networkAccess": "none", "searchKeywords": [ "pipeline", "recipe builder", @@ -2211,7 +2329,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "certificate_decoder", "base64_encode_decode", "security_header_analyzer" - ] + ], + "networkAccess": "none" }, "asn1_der_inspector": { "key": "asn1_der_inspector", @@ -2228,7 +2347,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "base64_encode_decode", "hex_bytes_workbench", "jwt_decoder" - ] + ], + "networkAccess": "none" }, "hex_bytes_workbench": { "key": "hex_bytes_workbench", @@ -2245,7 +2365,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_encode_decode", "hash_generator", "unicode_inspector" - ] + ], + "networkAccess": "none" }, "unicode_inspector": { "key": "unicode_inspector", @@ -2262,7 +2383,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "text_diff_checker", "hex_bytes_workbench", "multiple_whitespace_remover" - ] + ], + "networkAccess": "none" } } diff --git a/src/generated/tool-index.json b/src/generated/tool-index.json index 24ea9eb1..b0198571 100644 --- a/src/generated/tool-index.json +++ b/src/generated/tool-index.json @@ -1,9 +1,9 @@ { - "generatedAt": "2026-06-13T13:37:23.968Z", + "generatedAt": "2026-06-17T14:42:53.061Z", "counts": { "canonicalTools": 121, "aliasRoutes": 1, - "routeDirs": 169, + "routeDirs": 170, "unknownRouteDirs": 0 }, "canonicalTools": [ @@ -24,6 +24,7 @@ "json prettify", "format json online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/json-formatter/manifest.ts" }, @@ -44,6 +45,7 @@ "format xml online", "xml pretty print" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/xml-formatter/manifest.ts" }, @@ -63,6 +65,7 @@ "format sql online", "sql pretty print" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/sql-formatter/manifest.ts" }, @@ -83,6 +86,7 @@ "beautify javascript", "format javascript online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/javascript-formatter/manifest.ts" }, @@ -103,6 +107,7 @@ "minify javascript", "compress javascript online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/javascript-minifier/manifest.ts" }, @@ -123,6 +128,7 @@ "html compressor", "compress html online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/html-minifier/manifest.ts" }, @@ -143,6 +149,7 @@ "html entities encode", "html entities decode" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/html-encoder-decoder/manifest.ts" }, @@ -163,6 +170,7 @@ "html formatter online", "beautify html css" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/html-css-beautifier/manifest.ts" }, @@ -183,6 +191,7 @@ "format html online", "html pretty print" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/html-formatter/manifest.ts" }, @@ -203,6 +212,7 @@ "yaml converter online", "yaml json transform" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/yaml-json-converter/manifest.ts" }, @@ -223,6 +233,7 @@ "markdown to html", "gfm preview" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/markdown-preview/manifest.ts" }, @@ -243,6 +254,7 @@ "html markdown converter", "html2md online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/html-to-markdown/manifest.ts" }, @@ -262,6 +274,7 @@ "generate typescript types", "json to ts" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/json-to-typescript/manifest.ts" }, @@ -281,6 +294,7 @@ "css compressor", "css optimize" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-minifier/manifest.ts" }, @@ -300,6 +314,7 @@ "optimize svg", "compress svg" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/svg-optimizer/manifest.ts" }, @@ -320,6 +335,7 @@ "jsonpath query", "json path online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/jsonpath-playground/manifest.ts" }, @@ -339,6 +355,7 @@ "openapi explorer", "api spec viewer" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/openapi-viewer/manifest.ts" }, @@ -358,6 +375,7 @@ "json diff viewer", "json compare tool" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/json-diff-viewer/manifest.ts" }, @@ -378,6 +396,7 @@ "csv converter online", "csv json transform" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/csv-json-converter/manifest.ts" }, @@ -397,6 +416,7 @@ "base64 decode", "base64 converter online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/base64-encode-decode/manifest.ts" }, @@ -416,6 +436,7 @@ "percent encoding", "url encoder online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/url-encode-decode/manifest.ts" }, @@ -436,6 +457,7 @@ "jwt parser", "json web token decoder" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/jwt-decoder/manifest.ts" }, @@ -456,6 +478,7 @@ "jwt hs256 signer", "jwt all in one tool" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/jwt-workbench/manifest.ts" }, @@ -476,6 +499,7 @@ "sha256 hash online", "sha512 generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/hash-generator/manifest.ts" }, @@ -496,6 +520,7 @@ "md5 checksum", "md5 encoder" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/md5-generator/manifest.ts" }, @@ -515,6 +540,7 @@ "diff checker", "text comparison tool" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/text-diff-checker/manifest.ts" }, @@ -535,6 +561,7 @@ "collapse whitespace", "trim text online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/multiple-whitespace-remover/manifest.ts" }, @@ -555,6 +582,7 @@ "word counter", "text analyzer" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/letter-counter/manifest.ts" }, @@ -575,6 +603,7 @@ "bionic text generator", "focus reading tool" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/bionic-reading-converter/manifest.ts" }, @@ -595,6 +624,7 @@ "typography pairing", "font combination tool" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/google-fonts-pair-finder/manifest.ts" }, @@ -615,6 +645,7 @@ "typed text to handwritten", "handwriting png" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/text-to-handwriting-converter/manifest.ts" }, @@ -635,6 +666,7 @@ "source code screenshot", "code image generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/code-to-image-converter/manifest.ts" }, @@ -653,6 +685,7 @@ "base64 to image", "convert image base64 online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/image-base64/manifest.ts" }, @@ -672,6 +705,7 @@ "timestamp to date", "unix time online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/unix-timestamp/manifest.ts" }, @@ -691,6 +725,7 @@ "generate uuid online", "guid generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/uuid-generator/manifest.ts" }, @@ -709,6 +744,7 @@ "dummy text generator", "placeholder text" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/lorem-ipsum/manifest.ts" }, @@ -727,6 +763,7 @@ "random password", "strong password generator online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/password-generator/manifest.ts" }, @@ -746,6 +783,7 @@ "hsl converter", "color picker online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/color-converter/manifest.ts" }, @@ -766,6 +804,7 @@ "android elevation generator", "react native style shadow" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/react-native-shadow-generator/manifest.ts" }, @@ -786,6 +825,7 @@ "color scheme creator", "palette maker online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/ai-color-palette-generator/manifest.ts" }, @@ -806,6 +846,7 @@ "rgb color blend", "hsl color blend" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/color-mixer/manifest.ts" }, @@ -826,6 +867,7 @@ "color ramp generator", "generate color tokens" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/color-shades-generator/manifest.ts" }, @@ -846,6 +888,7 @@ "dominant average hex", "image color analyzer" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/image-average-color-finder/manifest.ts" }, @@ -866,6 +909,7 @@ "caption maker", "image quote generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/image-caption-generator/manifest.ts" }, @@ -886,6 +930,7 @@ "image palette generator", "dominant colors from photo" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/image-color-extractor/manifest.ts" }, @@ -906,6 +951,7 @@ "image hex picker", "sample image color" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/image-color-picker/manifest.ts" }, @@ -926,6 +972,7 @@ "photo crop tool", "crop png jpg" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/image-cropper/manifest.ts" }, @@ -946,6 +993,7 @@ "adjust brightness contrast image", "image effects online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/image-filters/manifest.ts" }, @@ -966,6 +1014,7 @@ "instagram style photo editor", "photo preset filters" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/instagram-filters/manifest.ts" }, @@ -986,6 +1035,7 @@ "social post generator", "instagram post template" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/instagram-post-generator/manifest.ts" }, @@ -1006,6 +1056,7 @@ "story mockup generator", "social story maker" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/instagram-story-generator/manifest.ts" }, @@ -1026,6 +1077,7 @@ "twitter card meta tags", "social preview meta" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/open-graph-meta-generator/manifest.ts" }, @@ -1046,6 +1098,7 @@ "tweet image maker", "social tweet mockup" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/tweet-generator/manifest.ts" }, @@ -1066,6 +1119,7 @@ "tweet image converter", "x post image maker" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/tweet-to-image-converter/manifest.ts" }, @@ -1086,6 +1140,7 @@ "social ad revenue forecast", "cpm ctr calculator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/twitter-ad-revenue-generator/manifest.ts" }, @@ -1106,6 +1161,7 @@ "instagram media saver", "authorized ig image download" ], + "networkAccess": "user_requested", "updatedAt": null, "sourceFile": "src/features/tools/instagram-photo-downloader/manifest.ts" }, @@ -1126,6 +1182,7 @@ "vimeo cover image", "get vimeo video thumbnail" ], + "networkAccess": "user_requested", "updatedAt": null, "sourceFile": "src/features/tools/vimeo-thumbnail-grabber/manifest.ts" }, @@ -1146,6 +1203,7 @@ "youtube video thumbnail", "get youtube thumbnail url" ], + "networkAccess": "user_requested", "updatedAt": null, "sourceFile": "src/features/tools/youtube-thumbnail-grabber/manifest.ts" }, @@ -1166,6 +1224,7 @@ "change photo dimensions", "image resize tool" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/image-resizer/manifest.ts" }, @@ -1186,6 +1245,7 @@ "pixelate image region", "hide sensitive info image" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/photo-censor/manifest.ts" }, @@ -1206,6 +1266,7 @@ "enhance scanned document", "multi page pdf exporter" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/scanned-pdf-converter/manifest.ts" }, @@ -1226,6 +1287,7 @@ "blob path generator", "random blob svg" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/svg-blob-generator/manifest.ts" }, @@ -1246,6 +1308,7 @@ "tileable pattern svg", "svg texture generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/svg-pattern-generator/manifest.ts" }, @@ -1266,6 +1329,7 @@ "outline to fill svg", "svg path converter" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/svg-stroke-to-fill-converter/manifest.ts" }, @@ -1286,6 +1350,7 @@ "svg rasterizer", "export svg as png" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/svg-to-png-converter/manifest.ts" }, @@ -1306,6 +1371,7 @@ "css background maker", "pattern css online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-background-pattern-generator/manifest.ts" }, @@ -1326,6 +1392,7 @@ "border radius css", "radius generator online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-border-radius-generator/manifest.ts" }, @@ -1346,6 +1413,7 @@ "css shadow builder", "box shadow css online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-box-shadow-generator/manifest.ts" }, @@ -1366,6 +1434,7 @@ "checkbox style maker", "checkbox ui generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-checkbox-generator/manifest.ts" }, @@ -1386,6 +1455,7 @@ "clip-path css", "shape generator css" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-clip-path-generator/manifest.ts" }, @@ -1406,6 +1476,7 @@ "transition timing function", "cubic bezier tool" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-cubic-bezier-generator/manifest.ts" }, @@ -1426,6 +1497,7 @@ "frosted glass ui", "backdrop filter generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-glassmorphism-generator/manifest.ts" }, @@ -1446,6 +1518,7 @@ "radial gradient generator", "gradient maker online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-gradient-generator/manifest.ts" }, @@ -1466,6 +1539,7 @@ "loading animation css", "css preloaders" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-loader-generator/manifest.ts" }, @@ -1486,6 +1560,7 @@ "custom switch styles", "switch ui generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-switch-generator/manifest.ts" }, @@ -1506,6 +1581,7 @@ "neon glitch generator", "animated text effect css" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-text-glitch-effect-generator/manifest.ts" }, @@ -1526,6 +1602,7 @@ "tooltip arrow css", "directional triangle css" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/css-triangle-generator/manifest.ts" }, @@ -1544,6 +1621,7 @@ "generate qr code online", "qr code maker" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/qr-code-generator/manifest.ts" }, @@ -1564,6 +1642,7 @@ "ean13 generator", "barcode png svg export" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/barcode-generator/manifest.ts" }, @@ -1584,6 +1663,7 @@ "iban checksum generator", "dummy bank iban" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/fake-iban-generator/manifest.ts" }, @@ -1604,6 +1684,7 @@ "random sample picker", "seeded random list" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/list-randomizer/manifest.ts" }, @@ -1622,6 +1703,7 @@ "text to ascii art", "figlet online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/ascii-art-generator/manifest.ts" }, @@ -1641,6 +1723,7 @@ "parse .env file online", "environment variable viewer" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/env-parser/manifest.ts" }, @@ -1661,6 +1744,7 @@ "nanoid generator", "unique id generator online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/id-generator/manifest.ts" }, @@ -1680,6 +1764,7 @@ "test regular expression", "regex debugger" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/regex-tester/manifest.ts" }, @@ -1700,6 +1785,7 @@ "regex pattern builder", "regular expression generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/regex-generator/manifest.ts" }, @@ -1718,6 +1804,7 @@ "cron expression builder", "cron maker online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/crontab-generator/manifest.ts" }, @@ -1737,6 +1824,7 @@ "parse user agent", "ua string analyzer online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/user-agent-parser/manifest.ts" }, @@ -1755,6 +1843,7 @@ "cron schedule viewer", "visualize cron expression" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/cron-visualizer/manifest.ts" }, @@ -1774,6 +1863,7 @@ "http response codes list", "status code reference" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/http-status-codes/manifest.ts" }, @@ -1793,6 +1883,7 @@ "unix permissions calculator", "file permissions chmod" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/chmod-calculator/manifest.ts" }, @@ -1813,6 +1904,7 @@ "ipv4 cidr to range", "network broadcast calculator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/cidr-subnet-calculator/manifest.ts" }, @@ -1833,6 +1925,7 @@ "query string parser", "url components breakdown" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/url-parser/manifest.ts" }, @@ -1853,6 +1946,7 @@ "ssl certificate decoder online", "certificate parser" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/certificate-decoder/manifest.ts" }, @@ -1873,6 +1967,7 @@ "api request builder", "rest client online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/http-request-builder/manifest.ts" }, @@ -1893,6 +1988,7 @@ "curl to python", "curl converter online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/curl-to-code/manifest.ts" }, @@ -1913,6 +2009,7 @@ "jsonl formatter", "ndjson to json array" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/ndjson-formatter/manifest.ts" }, @@ -1934,6 +2031,7 @@ "hmac jwt verify", "jwt validator online" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/jwt-verifier/manifest.ts" }, @@ -1954,6 +2052,7 @@ "camelcase converter", "snake case to camel case" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/slugify-case-converter/manifest.ts" }, @@ -1975,6 +2074,7 @@ "unicode detector", "hidden characters finder" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/invisible-characters-detector/manifest.ts" }, @@ -1995,6 +2095,7 @@ "robots txt validator", "crawler access tester" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/robots-txt-tester/manifest.ts" }, @@ -2015,6 +2116,7 @@ "csp validator", "csp checker" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/csp-parser/manifest.ts" }, @@ -2035,6 +2137,7 @@ "csv difference checker", "spreadsheet diff" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/csv-diff/manifest.ts" }, @@ -2055,6 +2158,7 @@ "request diff", "header comparison" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/header-diff/manifest.ts" }, @@ -2075,6 +2179,7 @@ "csp hsts checker", "header security audit" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/security-header-analyzer/manifest.ts" }, @@ -2095,6 +2200,7 @@ "2fa code", "authenticator code generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/totp-generator/manifest.ts" }, @@ -2115,6 +2221,7 @@ "api mock generator", "mock response generator" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/openapi-mock/manifest.ts" }, @@ -2135,6 +2242,7 @@ "docker command to yaml", "containerize" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/docker-run-to-compose/manifest.ts" }, @@ -2156,6 +2264,7 @@ "ndjson logs", "log viewer" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/local-log-parser/manifest.ts" }, @@ -2177,6 +2286,7 @@ "jq filter", "json transform" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/jq-playground/manifest.ts" }, @@ -2197,6 +2307,7 @@ "secret redaction", "pii remover" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/log-scrubber/manifest.ts" }, @@ -2217,6 +2328,7 @@ "brotli compression", "http compression" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/gzip-brotli-lab/manifest.ts" }, @@ -2237,6 +2349,7 @@ "json merge patch", "kubernetes yaml" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/yaml-merge-patch-explorer/manifest.ts" }, @@ -2258,6 +2371,7 @@ "yq online", "yaml transform" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/yq-playground/manifest.ts" }, @@ -2279,6 +2393,7 @@ "xml tree", "data graph" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/structured-data-visualizer/manifest.ts" }, @@ -2300,6 +2415,7 @@ "network log sanitizer", "har redaction" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/har-viewer-sanitizer/manifest.ts" }, @@ -2321,6 +2437,7 @@ "local workflow", "developer workbench" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/pipeline-builder/manifest.ts" }, @@ -2341,6 +2458,7 @@ "saml request parser", "saml assertion viewer" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/saml-decoder/manifest.ts" }, @@ -2361,6 +2479,7 @@ "asn.1 viewer", "certificate der decoder" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/asn1-der-inspector/manifest.ts" }, @@ -2381,6 +2500,7 @@ "bytes inspector", "utf8 hex converter" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/hex-bytes-workbench/manifest.ts" }, @@ -2401,6 +2521,7 @@ "unicode analyzer", "utf8 inspector" ], + "networkAccess": "none", "updatedAt": null, "sourceFile": "src/features/tools/unicode-inspector/manifest.ts" } @@ -2431,6 +2552,14 @@ "inHubGroup": false, "inStaticGroup": false }, + { + "slug": "all-tools", + "hasPage": true, + "kind": "static-content", + "targetSlug": null, + "inHubGroup": false, + "inStaticGroup": true + }, { "slug": "api-auth-header-mistakes", "hasPage": true, diff --git a/tests/guards/tool-network-access-metadata.test.ts b/tests/guards/tool-network-access-metadata.test.ts new file mode 100644 index 00000000..677e08a9 --- /dev/null +++ b/tests/guards/tool-network-access-metadata.test.ts @@ -0,0 +1,74 @@ +import fs from "node:fs" +import path from "node:path" +import { describe, expect, it } from "vitest" +import { TOOL_MANIFESTS } from "@/core/registry" +import type { ToolMeta } from "@/core/registry/types" + +const ROOT = process.cwd() +const FEATURE_TOOLS_DIR = path.join(ROOT, "src/features/tools") +const SOURCE_EXTENSIONS = new Set([".ts", ".tsx"]) +const EXECUTABLE_NETWORK_PATTERNS = [ + /\bfetch\s*\(/, + /\bwindow\.open\s*\(/, + /\bnavigator\.sendBeacon\s*\(/, +] +const EXTERNAL_TARGET_PATTERNS = [ + /\btarget\s*=\s*["']_blank["']/, + /\.\s*target\s*=\s*["']_blank["']/, +] + +function walkFiles(dir: string): string[] { + if (!fs.existsSync(dir)) return [] + + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) return walkFiles(fullPath) + if (!entry.isFile() || !SOURCE_EXTENSIONS.has(path.extname(entry.name))) return [] + if (entry.name === "manifest.ts") return [] + return [fullPath] + }) +} + +function stripCommentsAndStrings(source: string): string { + return source + .replace(/\/\*[\s\S]*?\*\//g, " ") + .replace(/\/\/.*$/gm, " ") + .replace(/`(?:\\.|[^`\\])*`/g, "\"\"") + .replace(/"((?:\\.|[^"\\])*)"/g, "\"\"") + .replace(/'((?:\\.|[^'\\])*)'/g, "''") +} + +function hasNetworkBehavior(source: string): boolean { + const executableSource = stripCommentsAndStrings(source) + return ( + EXECUTABLE_NETWORK_PATTERNS.some((pattern) => pattern.test(executableSource)) || + EXTERNAL_TARGET_PATTERNS.some((pattern) => pattern.test(source)) + ) +} + +describe("tool network access metadata", () => { + it("requires explicit metadata for tools with browser network behavior", () => { + const tools = TOOL_MANIFESTS as readonly ToolMeta[] + const manifestBySlug = new Map(tools.map((tool) => [tool.slug, tool])) + const offenders = [...manifestBySlug.values()].flatMap((tool) => { + const files = walkFiles(path.join(FEATURE_TOOLS_DIR, tool.slug)) + const hasNetwork = files.some((file) => hasNetworkBehavior(fs.readFileSync(file, "utf8"))) + return hasNetwork && !tool.networkAccess ? [`${tool.slug}: missing networkAccess`] : [] + }) + + expect(offenders).toEqual([]) + }) + + it("does not mark network-free tools as requiring external access", () => { + const tools = TOOL_MANIFESTS as readonly ToolMeta[] + const manifestBySlug = new Map(tools.map((tool) => [tool.slug, tool])) + const offenders = [...manifestBySlug.values()].flatMap((tool) => { + if (!tool.networkAccess || tool.networkAccess === "none") return [] + const files = walkFiles(path.join(FEATURE_TOOLS_DIR, tool.slug)) + const hasNetwork = files.some((file) => hasNetworkBehavior(fs.readFileSync(file, "utf8"))) + return hasNetwork ? [] : [`${tool.slug}: declares ${tool.networkAccess} but no guarded network behavior was found`] + }) + + expect(offenders).toEqual([]) + }) +}) From ac55cb4a6411e66b14b5c2482d6338e1acceecb4 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 07:46:12 -0700 Subject: [PATCH 04/28] feat(storage): add input persistence policy --- src/core/storage/tool-persistence-policy.ts | 29 +++++++++++++++++++ .../tools/json-formatter/constants.ts | 5 ++++ src/features/tools/json-formatter/page.tsx | 24 +++++---------- .../json-formatter-persistence-policy.test.ts | 17 +++++++++++ tests/unit/tool-persistence.test.ts | 19 ++++++++++++ 5 files changed, 77 insertions(+), 17 deletions(-) create mode 100644 src/core/storage/tool-persistence-policy.ts create mode 100644 tests/guards/json-formatter-persistence-policy.test.ts diff --git a/src/core/storage/tool-persistence-policy.ts b/src/core/storage/tool-persistence-policy.ts new file mode 100644 index 00000000..599ff6a2 --- /dev/null +++ b/src/core/storage/tool-persistence-policy.ts @@ -0,0 +1,29 @@ +import { removeStorageKey, writeStorageString } from "./tool-persistence" + +export type ToolInputPersistenceMode = true | false | "opt-in" + +export type ToolPersistencePolicy = { + persistInput: ToolInputPersistenceMode + inputStorageKey?: string + maxInputChars?: number +} + +export function shouldPersistToolInput(policy: ToolPersistencePolicy): boolean { + return policy.persistInput === true +} + +export function enforceToolInputPersistencePolicy(policy: ToolPersistencePolicy, input: string): void { + if (!policy.inputStorageKey) return + + if (!shouldPersistToolInput(policy) || !input.trim()) { + removeStorageKey(policy.inputStorageKey) + return + } + + if (typeof policy.maxInputChars === "number" && input.length > policy.maxInputChars) { + removeStorageKey(policy.inputStorageKey) + return + } + + writeStorageString(policy.inputStorageKey, input) +} diff --git a/src/features/tools/json-formatter/constants.ts b/src/features/tools/json-formatter/constants.ts index d4a5db0c..d017704e 100644 --- a/src/features/tools/json-formatter/constants.ts +++ b/src/features/tools/json-formatter/constants.ts @@ -2,3 +2,8 @@ export const VIEW_MODE_STORAGE_KEY = "byteflow:json-formatter:view-mode" export const INPUT_STORAGE_KEY = "byteflow:json-formatter:input" export const INPUT_STORAGE_DEBOUNCE_MS = 700 export const INPUT_STORAGE_MAX_CHARS = 2_000_000 +export const JSON_FORMATTER_PERSISTENCE_POLICY = { + persistInput: false, + inputStorageKey: INPUT_STORAGE_KEY, + maxInputChars: INPUT_STORAGE_MAX_CHARS, +} as const diff --git a/src/features/tools/json-formatter/page.tsx b/src/features/tools/json-formatter/page.tsx index 7f066d89..38ea9f6a 100644 --- a/src/features/tools/json-formatter/page.tsx +++ b/src/features/tools/json-formatter/page.tsx @@ -28,14 +28,15 @@ import { ToolActionBar, type ToolAction } from "@/features/tool-shell/tool-actio import { ToolEmptyState } from "@/features/tool-shell/tool-empty-state" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" -import { readStorageString, writeStorageString, removeStorageKey } from "@/core/storage/tool-persistence" +import { readStorageString, writeStorageString, removeStorageKey } from "@/core/storage/tool-persistence" +import { enforceToolInputPersistencePolicy } from "@/core/storage/tool-persistence-policy" import { importTextFile, TEXT_FILE_IMPORT_ACCEPT } from "@/core/files/text-file-import" import { buildToolHandoffLink, getToolHandoffFromSearchParams } from "@/core/routing/tool-handoff" import { safeClipboardWrite } from "@/core/clipboard/clipboard" import { buildJsonParseErrorMessage } from "@/features/tools/json-formatter/error-utils" import { PrivacyBadge } from "@/features/tool-shell/privacy-badge" import { PrivacyFAQ } from "@/features/tool-shell/privacy-faq" -import { INPUT_STORAGE_DEBOUNCE_MS, INPUT_STORAGE_KEY, INPUT_STORAGE_MAX_CHARS, VIEW_MODE_STORAGE_KEY } from "./constants" +import { INPUT_STORAGE_DEBOUNCE_MS, INPUT_STORAGE_KEY, JSON_FORMATTER_PERSISTENCE_POLICY, VIEW_MODE_STORAGE_KEY } from "./constants" import { findMatchingPaths, getAllPaths, @@ -91,11 +92,8 @@ export function JsonFormatterPage() { setViewMode(savedMode) } - const savedInput = readStorageString(INPUT_STORAGE_KEY) - if (savedInput !== null) { - setInput(savedInput) - } - }, []) + removeStorageKey(INPUT_STORAGE_KEY) + }, []) React.useEffect(() => { writeStorageString(VIEW_MODE_STORAGE_KEY, viewMode) @@ -116,16 +114,8 @@ export function JsonFormatterPage() { React.useEffect(() => { const timeoutId = window.setTimeout(() => { - if (!input.trim()) { - removeStorageKey(INPUT_STORAGE_KEY) - return - } - if (input.length > INPUT_STORAGE_MAX_CHARS) { - removeStorageKey(INPUT_STORAGE_KEY) - return - } - writeStorageString(INPUT_STORAGE_KEY, input) - }, INPUT_STORAGE_DEBOUNCE_MS) + enforceToolInputPersistencePolicy(JSON_FORMATTER_PERSISTENCE_POLICY, input) + }, INPUT_STORAGE_DEBOUNCE_MS) return () => window.clearTimeout(timeoutId) }, [input]) diff --git a/tests/guards/json-formatter-persistence-policy.test.ts b/tests/guards/json-formatter-persistence-policy.test.ts new file mode 100644 index 00000000..6e0ee029 --- /dev/null +++ b/tests/guards/json-formatter-persistence-policy.test.ts @@ -0,0 +1,17 @@ +import fs from "node:fs" +import path from "node:path" +import { describe, expect, it } from "vitest" +import { JSON_FORMATTER_PERSISTENCE_POLICY } from "@/features/tools/json-formatter/constants" + +describe("json formatter persistence policy", () => { + it("does not persist JSON payload input by default", () => { + expect(JSON_FORMATTER_PERSISTENCE_POLICY.persistInput).toBe(false) + }) + + it("uses the central persistence policy instead of directly writing input", () => { + const source = fs.readFileSync(path.join(process.cwd(), "src/features/tools/json-formatter/page.tsx"), "utf8") + + expect(source).toContain("enforceToolInputPersistencePolicy(JSON_FORMATTER_PERSISTENCE_POLICY, input)") + expect(source).not.toContain("writeStorageString(INPUT_STORAGE_KEY, input)") + }) +}) diff --git a/tests/unit/tool-persistence.test.ts b/tests/unit/tool-persistence.test.ts index f9b41f46..c707842c 100644 --- a/tests/unit/tool-persistence.test.ts +++ b/tests/unit/tool-persistence.test.ts @@ -6,6 +6,7 @@ import { writeStorageJson, writeStorageString, } from "@/core/storage/tool-persistence" +import { enforceToolInputPersistencePolicy, shouldPersistToolInput } from "@/core/storage/tool-persistence-policy" describe("tool-persistence", () => { beforeEach(() => { @@ -50,4 +51,22 @@ describe("tool-persistence", () => { removeStorageKey(key) expect(readStorageString(key)).toBeNull() }) + + it("enforces input persistence policy", () => { + const key = "byteflow:test:policy" + + expect(shouldPersistToolInput({ persistInput: false })).toBe(false) + expect(shouldPersistToolInput({ persistInput: "opt-in" })).toBe(false) + expect(shouldPersistToolInput({ persistInput: true })).toBe(true) + + enforceToolInputPersistencePolicy({ persistInput: true, inputStorageKey: key, maxInputChars: 10 }, "payload") + expect(readStorageString(key)).toBe("payload") + + enforceToolInputPersistencePolicy({ persistInput: true, inputStorageKey: key, maxInputChars: 3 }, "payload") + expect(readStorageString(key)).toBeNull() + + writeStorageString(key, "old") + enforceToolInputPersistencePolicy({ persistInput: false, inputStorageKey: key }, "payload") + expect(readStorageString(key)).toBeNull() + }) }) From ad5b1892003360896b46075cc582a7c31df61793 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 07:49:10 -0700 Subject: [PATCH 05/28] chore(seo): refresh sitemap lastmod manifest --- src/lib/sitemap-lastmod.json | 1702 +++++++++++++++++----------------- 1 file changed, 851 insertions(+), 851 deletions(-) diff --git a/src/lib/sitemap-lastmod.json b/src/lib/sitemap-lastmod.json index 54623f99..121ba49c 100644 --- a/src/lib/sitemap-lastmod.json +++ b/src/lib/sitemap-lastmod.json @@ -456,1093 +456,1093 @@ }, "tools": { "json-formatter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "xml-formatter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "sql-formatter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "javascript-formatter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "javascript-minifier": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "html-minifier": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "html-encoder-decoder": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "html-css-beautifier": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "html-formatter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "yaml-json-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "markdown-preview": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "html-to-markdown": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "json-to-typescript": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-minifier": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "svg-optimizer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "jsonpath-playground": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "openapi-viewer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "json-diff-viewer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "csv-json-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "base64-encode-decode": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "url-encode-decode": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "jwt-decoder": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "jwt-workbench": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "hash-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "md5-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "text-diff-checker": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "multiple-whitespace-remover": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "letter-counter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "bionic-reading-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "google-fonts-pair-finder": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "text-to-handwriting-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "code-to-image-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "image-base64": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "unix-timestamp": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "uuid-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "lorem-ipsum": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "password-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "color-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "react-native-shadow-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "ai-color-palette-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "color-mixer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "color-shades-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "image-average-color-finder": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "image-caption-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "image-color-extractor": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "image-color-picker": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "image-cropper": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "image-filters": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "instagram-filters": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "instagram-post-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "instagram-story-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "open-graph-meta-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "tweet-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "tweet-to-image-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "twitter-ad-revenue-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "instagram-photo-downloader": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "vimeo-thumbnail-grabber": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "youtube-thumbnail-grabber": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "image-resizer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "photo-censor": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "scanned-pdf-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "svg-blob-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "svg-pattern-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "svg-stroke-to-fill-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "svg-to-png-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-background-pattern-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-border-radius-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-box-shadow-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-checkbox-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-clip-path-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-cubic-bezier-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-glassmorphism-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-gradient-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-loader-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-switch-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-text-glitch-effect-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "css-triangle-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "qr-code-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "barcode-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "fake-iban-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "list-randomizer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "ascii-art-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "env-parser": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" - }, - "id-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" - }, - "regex-tester": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" + }, + "id-generator": { + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" + }, + "regex-tester": { + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "regex-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "crontab-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "user-agent-parser": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "cron-visualizer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "http-status-codes": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "chmod-calculator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "cidr-subnet-calculator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "url-parser": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "certificate-decoder": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "http-request-builder": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "curl-to-code": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "ndjson-formatter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "jwt-verifier": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "slugify-case-converter": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "invisible-characters-detector": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "robots-txt-tester": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "csp-parser": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "csv-diff": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "header-diff": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "security-header-analyzer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "totp-generator": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "openapi-mock": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "docker-run-to-compose": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "local-log-parser": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "jq-playground": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "log-scrubber": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "gzip-brotli-lab": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "yaml-merge-patch-explorer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "yq-playground": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "structured-data-visualizer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "har-viewer-sanitizer": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "pipeline-builder": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "saml-decoder": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "asn1-der-inspector": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "hex-bytes-workbench": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" }, "unicode-inspector": { - "en": "2026-06-14T00:00:00.000Z", - "zh-CN": "2026-06-14T00:00:00.000Z", - "zh-TW": "2026-06-14T00:00:00.000Z", - "ja": "2026-06-14T00:00:00.000Z", - "ko": "2026-06-14T00:00:00.000Z", - "de": "2026-06-14T00:00:00.000Z", - "fr": "2026-06-14T00:00:00.000Z" + "en": "2026-06-17T00:00:00.000Z", + "zh-CN": "2026-06-17T00:00:00.000Z", + "zh-TW": "2026-06-17T00:00:00.000Z", + "ja": "2026-06-17T00:00:00.000Z", + "ko": "2026-06-17T00:00:00.000Z", + "de": "2026-06-17T00:00:00.000Z", + "fr": "2026-06-17T00:00:00.000Z" } } } From 4a92ae7dbe600c9366149d5e0a9477b03b7b5ad5 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 07:59:54 -0700 Subject: [PATCH 06/28] feat(storage): mark sensitive tools non-persistent --- .../generators/generate-client-tool-lookup.js | 2 + scripts/generators/generate-tool-index.js | 1 + scripts/lib/tool-manifest-lib.js | 14 + src/app/[lang]/privacy/page.tsx | 4 +- src/core/registry/types.ts | 3 + src/core/storage/tool-persistence-policy.ts | 22 ++ src/features/privacy/local-data-controls.tsx | 34 ++ .../tools/certificate-decoder/manifest.ts | 1 + src/features/tools/env-parser/manifest.ts | 1 + .../tools/har-viewer-sanitizer/manifest.ts | 1 + src/features/tools/header-diff/manifest.ts | 1 + .../tools/http-request-builder/manifest.ts | 1 + src/features/tools/json-formatter/manifest.ts | 1 + src/features/tools/jwt-decoder/manifest.ts | 1 + src/features/tools/jwt-verifier/manifest.ts | 1 + src/features/tools/jwt-workbench/manifest.ts | 1 + .../tools/local-log-parser/manifest.ts | 1 + src/features/tools/log-scrubber/manifest.ts | 1 + src/features/tools/saml-decoder/manifest.ts | 1 + .../security-header-analyzer/manifest.ts | 1 + src/features/tools/totp-generator/manifest.ts | 1 + src/generated/client-tool-lookup.ts | 324 ++++++++++++------ src/generated/tool-index.json | 123 ++++++- .../sensitive-tool-persistence-policy.test.ts | 33 ++ 24 files changed, 471 insertions(+), 103 deletions(-) create mode 100644 src/features/privacy/local-data-controls.tsx create mode 100644 tests/guards/sensitive-tool-persistence-policy.test.ts diff --git a/scripts/generators/generate-client-tool-lookup.js b/scripts/generators/generate-client-tool-lookup.js index 0b835383..ed658f48 100644 --- a/scripts/generators/generate-client-tool-lookup.js +++ b/scripts/generators/generate-client-tool-lookup.js @@ -67,6 +67,7 @@ function buildClientLookupSource() { aliases: aliasesByKey.get(tool.key) || [], relatedToolKeys: tool.relatedTools, networkAccess: tool.networkAccess || "none", + persistInput: tool.persistInput ?? null, } if (tool.searchKeywords) { entry.searchKeywords = tool.searchKeywords @@ -98,6 +99,7 @@ export type ClientToolLookupEntry = { aliases: readonly string[] relatedToolKeys: readonly string[] networkAccess: "none" | "user_requested" | "third_party_api" + persistInput: true | false | "opt-in" | null searchKeywords?: readonly string[] } diff --git a/scripts/generators/generate-tool-index.js b/scripts/generators/generate-tool-index.js index 876e9a46..9662ae26 100644 --- a/scripts/generators/generate-tool-index.js +++ b/scripts/generators/generate-tool-index.js @@ -169,6 +169,7 @@ function buildIndexData() { relatedTools: tool.relatedTools, keywords: tool.keywords, networkAccess: tool.networkAccess || "none", + persistInput: tool.persistInput ?? null, updatedAt: tool.updatedAt || null, sourceFile: tool.sourceFile, } diff --git a/scripts/lib/tool-manifest-lib.js b/scripts/lib/tool-manifest-lib.js index 4fcf9ecf..9650ea63 100644 --- a/scripts/lib/tool-manifest-lib.js +++ b/scripts/lib/tool-manifest-lib.js @@ -7,6 +7,7 @@ export const TOOL_MANIFESTS_PATH = path.join(ROOT_DIR, "src/core/registry/manife const REQUIRED_FIELDS = ["key", "slug", "category", "relatedTools", "keywords"] const NETWORK_ACCESS_VALUES = new Set(["none", "user_requested", "third_party_api"]) +const PERSIST_INPUT_VALUES = new Set(["true", "false", "\"opt-in\"", "'opt-in'"]) function relative(filePath) { return path.relative(ROOT_DIR, filePath).replace(/\\/g, "/") @@ -195,6 +196,17 @@ function objectField(source, fieldName) { return match ? match[1] : null } +function booleanOrOptInField(source, fieldName, manifestPath) { + const match = new RegExp(`(?:^|[,\\n])\\s*${fieldName}:\\s*(true|false|["']opt-in["'])`, "s").exec(source) + if (!match) return undefined + if (!PERSIST_INPUT_VALUES.has(match[1])) { + throw manifestError(manifestPath, fieldName, "must be true, false, or \"opt-in\"") + } + if (match[1] === "true") return true + if (match[1] === "false") return false + return "opt-in" +} + function parseDeprecated(source, manifestPath) { const block = objectField(source, "deprecated") if (!block) return undefined @@ -238,6 +250,7 @@ export function parseToolManifestFile(manifestPath) { const searchKeywords = arrayField(body, "searchKeywords", manifestPath) const updatedAt = stringField(body, "updatedAt", manifestPath) const networkAccess = stringField(body, "networkAccess", manifestPath) + const persistInput = booleanOrOptInField(body, "persistInput", manifestPath) const deprecated = parseDeprecated(body, manifestPath) if (networkAccess && !NETWORK_ACCESS_VALUES.has(networkAccess)) { @@ -256,6 +269,7 @@ export function parseToolManifestFile(manifestPath) { if (updatedAt) manifest.updatedAt = updatedAt if (searchKeywords.length > 0) manifest.searchKeywords = searchKeywords if (networkAccess) manifest.networkAccess = networkAccess + if (persistInput !== undefined) manifest.persistInput = persistInput if (deprecated) manifest.deprecated = deprecated return manifest diff --git a/src/app/[lang]/privacy/page.tsx b/src/app/[lang]/privacy/page.tsx index dad0cce9..6b519b10 100644 --- a/src/app/[lang]/privacy/page.tsx +++ b/src/app/[lang]/privacy/page.tsx @@ -1,6 +1,7 @@ "use client" import { useLang } from "@/core/i18n/lang-provider" +import { LocalDataControls } from "@/features/privacy/local-data-controls" export default function PrivacyPage() { const { t } = useLang() @@ -28,7 +29,8 @@ export default function PrivacyPage() {

{section.desc}

))} + ) -} \ No newline at end of file +} diff --git a/src/core/registry/types.ts b/src/core/registry/types.ts index bc89f9b1..7bc2dd5d 100644 --- a/src/core/registry/types.ts +++ b/src/core/registry/types.ts @@ -3,6 +3,7 @@ import type { ToolCategory } from "./categories" export type { ToolCategory } from "./categories" export type ToolNetworkAccess = "none" | "user_requested" | "third_party_api" +export type ToolInputPersistenceMode = true | false | "opt-in" /** * Tool metadata used by registry, sitemap, SEO, breadcrumbs, and related tools. @@ -24,6 +25,8 @@ export interface ToolMeta { searchKeywords?: string[] /** Browser network behavior used by privacy UI and CI guards */ networkAccess?: ToolNetworkAccess + /** Input payload persistence behavior used by privacy UI and CI guards */ + persistInput?: ToolInputPersistenceMode /** Optional deprecation metadata - marks tool as deprecated with message and alternatives */ deprecated?: { /** Translation key for deprecation message (optional, falls back to generic message) */ diff --git a/src/core/storage/tool-persistence-policy.ts b/src/core/storage/tool-persistence-policy.ts index 599ff6a2..0c0106fe 100644 --- a/src/core/storage/tool-persistence-policy.ts +++ b/src/core/storage/tool-persistence-policy.ts @@ -27,3 +27,25 @@ export function enforceToolInputPersistencePolicy(policy: ToolPersistencePolicy, writeStorageString(policy.inputStorageKey, input) } + +export function clearByteflowLocalData(): number { + if (typeof window === "undefined") return 0 + + 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) => { + window.localStorage.removeItem(key) + removed += 1 + }) + } catch { + return removed + } + + return removed +} diff --git a/src/features/privacy/local-data-controls.tsx b/src/features/privacy/local-data-controls.tsx new file mode 100644 index 00000000..29f82c10 --- /dev/null +++ b/src/features/privacy/local-data-controls.tsx @@ -0,0 +1,34 @@ +"use client" + +import * as React from "react" +import { Trash2 } from "lucide-react" +import { Button } from "@/components/ui/button" +import { clearByteflowLocalData } from "@/core/storage/tool-persistence-policy" + +export function LocalDataControls() { + const [message, setMessage] = React.useState(null) + + const clearData = () => { + const removed = clearByteflowLocalData() + setMessage(removed > 0 ? `Cleared ${removed} saved Byteflow item${removed === 1 ? "" : "s"}.` : "No saved Byteflow data was found.") + } + + return ( +
+
+
+

Local data controls

+

+ Byteflow processing stays in your browser. Some preferences and non-sensitive tool settings may be saved locally; + sensitive payloads are configured not to persist by default. +

+
+ +
+ {message ?

{message}

: null} +
+ ) +} diff --git a/src/features/tools/certificate-decoder/manifest.ts b/src/features/tools/certificate-decoder/manifest.ts index 9c877724..41f0aa6c 100644 --- a/src/features/tools/certificate-decoder/manifest.ts +++ b/src/features/tools/certificate-decoder/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "network-web", relatedTools: ["jwt_decoder", "base64_encode_decode", "http_status_codes", "security_header_analyzer"], keywords: ["pem decoder", "x509 certificate viewer", "ssl certificate decoder online", "certificate parser"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/env-parser/manifest.ts b/src/features/tools/env-parser/manifest.ts index 3b78ec01..c6ed387f 100644 --- a/src/features/tools/env-parser/manifest.ts +++ b/src/features/tools/env-parser/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "generators", relatedTools: ["json_formatter", "yaml_json_converter", "base64_encode_decode"], keywords: ["env parser", "dotenv parser", "parse .env file online", "environment variable viewer"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/har-viewer-sanitizer/manifest.ts b/src/features/tools/har-viewer-sanitizer/manifest.ts index f77511e5..21552845 100644 --- a/src/features/tools/har-viewer-sanitizer/manifest.ts +++ b/src/features/tools/har-viewer-sanitizer/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "network-web", relatedTools: ["local_log_parser", "log_scrubber", "http_status_codes", "security_header_analyzer"], keywords: ["har viewer", "har sanitizer", "http archive", "network log sanitizer", "har redaction"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/header-diff/manifest.ts b/src/features/tools/header-diff/manifest.ts index 44c8c479..b84db469 100644 --- a/src/features/tools/header-diff/manifest.ts +++ b/src/features/tools/header-diff/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "network-web", relatedTools: ["http_status_codes", "http_request_builder", "csp_parser", "text_diff_checker"], keywords: ["header diff", "http header compare", "request diff", "header comparison"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/http-request-builder/manifest.ts b/src/features/tools/http-request-builder/manifest.ts index 3bf7cf39..0dc02005 100644 --- a/src/features/tools/http-request-builder/manifest.ts +++ b/src/features/tools/http-request-builder/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "network-web", relatedTools: ["url_parser", "http_status_codes", "url_encode_decode", "user_agent_parser"], keywords: ["http request builder", "curl generator", "api request builder", "rest client online"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/json-formatter/manifest.ts b/src/features/tools/json-formatter/manifest.ts index aa3c8ee1..4fa135da 100644 --- a/src/features/tools/json-formatter/manifest.ts +++ b/src/features/tools/json-formatter/manifest.ts @@ -7,4 +7,5 @@ export const toolManifest = { relatedTools: ["jsonpath_playground", "json_diff_viewer", "json_to_typescript", "yaml_json_converter"], keywords: ["json formatter", "json beautifier", "json prettify", "format json online"], searchKeywords: ["beautify", "pretty print", "validate", "美化", "格式化", "验证", "格式化", "フォーマット", "검증", "포맷"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/jwt-decoder/manifest.ts b/src/features/tools/jwt-decoder/manifest.ts index 601b5b7e..5174adb8 100644 --- a/src/features/tools/jwt-decoder/manifest.ts +++ b/src/features/tools/jwt-decoder/manifest.ts @@ -7,4 +7,5 @@ export const toolManifest = { relatedTools: ["jwt_workbench", "base64_encode_decode", "hash_generator", "url_encode_decode"], keywords: ["jwt decoder", "decode jwt online", "jwt parser", "json web token decoder"], searchKeywords: ["decode token", "parse jwt", "jwt inspector", "token decoder", "令牌解码", "トークンデコード", "JWT 파서", "解析令牌"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/jwt-verifier/manifest.ts b/src/features/tools/jwt-verifier/manifest.ts index 116f06e7..33be2b4e 100644 --- a/src/features/tools/jwt-verifier/manifest.ts +++ b/src/features/tools/jwt-verifier/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "text-string", relatedTools: ["jwt_workbench", "jwt_decoder", "hash_generator", "base64_encode_decode", "certificate_decoder"], keywords: ["jwt verify", "jwt signature verifier", "hmac jwt verify", "jwt validator online"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/jwt-workbench/manifest.ts b/src/features/tools/jwt-workbench/manifest.ts index 7babe285..362458de 100644 --- a/src/features/tools/jwt-workbench/manifest.ts +++ b/src/features/tools/jwt-workbench/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "text-string", relatedTools: ["jwt_decoder", "jwt_verifier", "base64_encode_decode", "hash_generator"], keywords: ["jwt workbench", "jwt encode decode verify", "jwt hs256 signer", "jwt all in one tool"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/local-log-parser/manifest.ts b/src/features/tools/local-log-parser/manifest.ts index 2ec011f1..b3edd704 100644 --- a/src/features/tools/local-log-parser/manifest.ts +++ b/src/features/tools/local-log-parser/manifest.ts @@ -7,4 +7,5 @@ export const toolManifest = { relatedTools: ["json_formatter", "ndjson_formatter", "regex_tester", "text_diff_checker"], keywords: ["log parser", "log analyzer", "json lines", "ndjson logs", "log viewer"], searchKeywords: ["parse logs", "analyze logs", "log level", "error logs", "日志解析", "ログ解析", "로그 파서"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/log-scrubber/manifest.ts b/src/features/tools/log-scrubber/manifest.ts index 5bb1dbaf..37cf1604 100644 --- a/src/features/tools/log-scrubber/manifest.ts +++ b/src/features/tools/log-scrubber/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "network-web", relatedTools: ["local_log_parser", "security_header_analyzer", "jwt_decoder", "text_diff_checker"], keywords: ["log scrubber", "log redaction", "secret redaction", "pii remover"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/saml-decoder/manifest.ts b/src/features/tools/saml-decoder/manifest.ts index 7a25cf83..0ba966f7 100644 --- a/src/features/tools/saml-decoder/manifest.ts +++ b/src/features/tools/saml-decoder/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "network-web", relatedTools: ["jwt_decoder", "certificate_decoder", "base64_encode_decode", "security_header_analyzer"], keywords: ["saml decoder", "saml response decoder", "saml request parser", "saml assertion viewer"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/security-header-analyzer/manifest.ts b/src/features/tools/security-header-analyzer/manifest.ts index 47ec9a30..7453e0c5 100644 --- a/src/features/tools/security-header-analyzer/manifest.ts +++ b/src/features/tools/security-header-analyzer/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "network-web", relatedTools: ["csp_parser", "header_diff", "http_status_codes", "certificate_decoder"], keywords: ["security header analyzer", "http security headers", "csp hsts checker", "header security audit"], + persistInput: false, } satisfies ToolMeta diff --git a/src/features/tools/totp-generator/manifest.ts b/src/features/tools/totp-generator/manifest.ts index 79469eec..0c84f1e0 100644 --- a/src/features/tools/totp-generator/manifest.ts +++ b/src/features/tools/totp-generator/manifest.ts @@ -6,4 +6,5 @@ export const toolManifest = { category: "generators", relatedTools: ["password_generator", "hash_generator", "uuid_generator", "id_generator"], keywords: ["totp generator", "hotp generator", "2fa code", "authenticator code generator"], + persistInput: false, } satisfies ToolMeta diff --git a/src/generated/client-tool-lookup.ts b/src/generated/client-tool-lookup.ts index f970fc52..e477d73e 100644 --- a/src/generated/client-tool-lookup.ts +++ b/src/generated/client-tool-lookup.ts @@ -10,6 +10,7 @@ export type ClientToolLookupEntry = { aliases: readonly string[] relatedToolKeys: readonly string[] networkAccess: "none" | "user_requested" | "third_party_api" + persistInput: true | false | "opt-in" | null searchKeywords?: readonly string[] } @@ -38,6 +39,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "yaml_json_converter" ], "networkAccess": "none", + "persistInput": false, "searchKeywords": [ "beautify", "pretty print", @@ -68,6 +70,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "sql_formatter" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "beautify xml", "format xml", @@ -94,6 +97,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "html_css_beautifier" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "beautify sql", "format sql", @@ -121,6 +125,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "markdown_preview" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "beautify js", "format javascript", @@ -147,7 +152,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "html_formatter", "json_formatter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "html_minifier": { "key": "html_minifier", @@ -165,7 +171,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_minifier", "javascript_minifier" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "html_encoder_decoder": { "key": "html_encoder_decoder", @@ -183,7 +190,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_encode_decode", "base64_encode_decode" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "html_css_beautifier": { "key": "html_css_beautifier", @@ -201,7 +209,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "svg_optimizer", "xml_formatter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "html_formatter": { "key": "html_formatter", @@ -220,6 +229,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "markdown_preview" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "beautify html", "format html", @@ -247,6 +257,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "xml_formatter" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "convert yaml", "yaml parser", @@ -274,6 +285,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "lorem_ipsum" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "preview markdown", "markdown viewer", @@ -300,7 +312,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "html_encoder_decoder", "text_diff_checker" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "json_to_typescript": { "key": "json_to_typescript", @@ -318,6 +331,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "jsonpath_playground" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "generate types", "interface", @@ -343,7 +357,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "svg_optimizer", "json_formatter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "svg_optimizer": { "key": "svg_optimizer", @@ -360,7 +375,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "html_css_beautifier", "image_base64" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "jsonpath_playground": { "key": "jsonpath_playground", @@ -378,7 +394,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_to_typescript", "yaml_json_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "openapi_viewer": { "key": "openapi_viewer", @@ -395,7 +412,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "yaml_json_converter", "jsonpath_playground" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "json_diff_viewer": { "key": "json_diff_viewer", @@ -412,7 +430,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "text_diff_checker", "jsonpath_playground" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "csv_json_converter": { "key": "csv_json_converter", @@ -430,7 +449,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_to_typescript", "text_diff_checker" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "base64_encode_decode": { "key": "base64_encode_decode", @@ -448,6 +468,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_base64" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "encode base64", "decode base64", @@ -480,6 +501,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "user_agent_parser" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "encode url", "decode url", @@ -511,6 +533,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_encode_decode" ], "networkAccess": "none", + "persistInput": false, "searchKeywords": [ "decode token", "parse jwt", @@ -538,7 +561,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "base64_encode_decode", "hash_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "hash_generator": { "key": "hash_generator", @@ -557,6 +581,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "password_generator" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "generate hash", "checksum", @@ -584,7 +609,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "text_diff_checker", "password_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "text_diff_checker": { "key": "text_diff_checker", @@ -601,7 +627,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "markdown_preview", "base64_encode_decode" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "multiple_whitespace_remover": { "key": "multiple_whitespace_remover", @@ -619,7 +646,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "markdown_preview", "lorem_ipsum" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "letter_counter": { "key": "letter_counter", @@ -637,7 +665,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "lorem_ipsum", "slugify_case_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "bionic_reading_converter": { "key": "bionic_reading_converter", @@ -655,7 +684,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "markdown_preview", "text_diff_checker" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "google_fonts_pair_finder": { "key": "google_fonts_pair_finder", @@ -673,7 +703,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "code_to_image_converter", "open_graph_meta_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "text_to_handwriting_converter": { "key": "text_to_handwriting_converter", @@ -691,7 +722,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_caption_generator", "instagram_post_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "code_to_image_converter": { "key": "code_to_image_converter", @@ -709,7 +741,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_formatter", "html_formatter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "image_base64": { "key": "image_base64", @@ -725,7 +758,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "svg_optimizer", "qr_code_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "unix_timestamp": { "key": "unix_timestamp", @@ -742,7 +776,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "cron_visualizer", "crontab_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "uuid_generator": { "key": "uuid_generator", @@ -760,6 +795,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "lorem_ipsum" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "generate uuid", "unique id", @@ -785,7 +821,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "uuid_generator", "markdown_preview" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "password_generator": { "key": "password_generator", @@ -801,7 +838,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "uuid_generator", "lorem_ipsum" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "color_converter": { "key": "color_converter", @@ -818,7 +856,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "html_css_beautifier", "svg_optimizer" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "react_native_shadow_generator": { "key": "react_native_shadow_generator", @@ -836,7 +875,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_border_radius_generator", "color_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "ai_color_palette_generator": { "key": "ai_color_palette_generator", @@ -854,7 +894,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_converter", "css_gradient_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "color_mixer": { "key": "color_mixer", @@ -872,7 +913,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "ai_color_palette_generator", "css_gradient_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "color_shades_generator": { "key": "color_shades_generator", @@ -890,7 +932,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_converter", "css_gradient_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "image_average_color_finder": { "key": "image_average_color_finder", @@ -908,7 +951,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_shades_generator", "color_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "image_caption_generator": { "key": "image_caption_generator", @@ -926,7 +970,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_average_color_finder", "color_mixer" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "image_color_extractor": { "key": "image_color_extractor", @@ -944,7 +989,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "ai_color_palette_generator", "color_mixer" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "image_color_picker": { "key": "image_color_picker", @@ -962,7 +1008,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_converter", "color_mixer" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "image_cropper": { "key": "image_cropper", @@ -980,7 +1027,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "code_to_image_converter", "image_base64" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "image_filters": { "key": "image_filters", @@ -998,7 +1046,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_caption_generator", "image_color_extractor" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "instagram_filters": { "key": "instagram_filters", @@ -1016,7 +1065,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_filters", "instagram_photo_downloader" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "instagram_post_generator": { "key": "instagram_post_generator", @@ -1034,7 +1084,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "instagram_filters", "open_graph_meta_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "instagram_story_generator": { "key": "instagram_story_generator", @@ -1052,7 +1103,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "instagram_filters", "open_graph_meta_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "open_graph_meta_generator": { "key": "open_graph_meta_generator", @@ -1070,7 +1122,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "instagram_post_generator", "instagram_story_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "tweet_generator": { "key": "tweet_generator", @@ -1088,7 +1141,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "twitter_ad_revenue_generator", "instagram_post_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "tweet_to_image_converter": { "key": "tweet_to_image_converter", @@ -1106,7 +1160,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "instagram_post_generator", "code_to_image_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "twitter_ad_revenue_generator": { "key": "twitter_ad_revenue_generator", @@ -1124,7 +1179,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "open_graph_meta_generator", "id_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "instagram_photo_downloader": { "key": "instagram_photo_downloader", @@ -1142,7 +1198,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "instagram_filters", "image_resizer" ], - "networkAccess": "user_requested" + "networkAccess": "user_requested", + "persistInput": null }, "vimeo_thumbnail_grabber": { "key": "vimeo_thumbnail_grabber", @@ -1160,7 +1217,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "open_graph_meta_generator", "image_resizer" ], - "networkAccess": "user_requested" + "networkAccess": "user_requested", + "persistInput": null }, "youtube_thumbnail_grabber": { "key": "youtube_thumbnail_grabber", @@ -1178,7 +1236,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "open_graph_meta_generator", "image_resizer" ], - "networkAccess": "user_requested" + "networkAccess": "user_requested", + "persistInput": null }, "image_resizer": { "key": "image_resizer", @@ -1196,7 +1255,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_base64", "code_to_image_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "photo_censor": { "key": "photo_censor", @@ -1214,7 +1274,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_resizer", "scanned_pdf_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "scanned_pdf_converter": { "key": "scanned_pdf_converter", @@ -1232,7 +1293,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "image_resizer", "image_base64" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "svg_blob_generator": { "key": "svg_blob_generator", @@ -1250,7 +1312,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "svg_optimizer", "css_clip_path_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "svg_pattern_generator": { "key": "svg_pattern_generator", @@ -1268,7 +1331,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_background_pattern_generator", "css_gradient_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "svg_stroke_to_fill_converter": { "key": "svg_stroke_to_fill_converter", @@ -1286,7 +1350,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "svg_pattern_generator", "svg_to_png_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "svg_to_png_converter": { "key": "svg_to_png_converter", @@ -1304,7 +1369,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "svg_blob_generator", "svg_stroke_to_fill_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_background_pattern_generator": { "key": "css_background_pattern_generator", @@ -1322,7 +1388,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_converter", "css_minifier" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_border_radius_generator": { "key": "css_border_radius_generator", @@ -1340,7 +1407,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_converter", "css_minifier" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_box_shadow_generator": { "key": "css_box_shadow_generator", @@ -1358,7 +1426,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_converter", "css_minifier" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_checkbox_generator": { "key": "css_checkbox_generator", @@ -1376,7 +1445,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_background_pattern_generator", "color_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_clip_path_generator": { "key": "css_clip_path_generator", @@ -1394,7 +1464,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "color_converter", "svg_optimizer" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_cubic_bezier_generator": { "key": "css_cubic_bezier_generator", @@ -1412,7 +1483,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_checkbox_generator", "css_minifier" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_glassmorphism_generator": { "key": "css_glassmorphism_generator", @@ -1430,7 +1502,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_border_radius_generator", "color_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_gradient_generator": { "key": "css_gradient_generator", @@ -1448,7 +1521,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_background_pattern_generator", "css_minifier" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_loader_generator": { "key": "css_loader_generator", @@ -1466,7 +1540,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_box_shadow_generator", "css_minifier" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_switch_generator": { "key": "css_switch_generator", @@ -1484,7 +1559,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_glassmorphism_generator", "color_converter" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_text_glitch_effect_generator": { "key": "css_text_glitch_effect_generator", @@ -1502,7 +1578,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "code_to_image_converter", "css_minifier" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "css_triangle_generator": { "key": "css_triangle_generator", @@ -1520,7 +1597,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "css_border_radius_generator", "css_minifier" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "qr_code_generator": { "key": "qr_code_generator", @@ -1536,7 +1614,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "password_generator", "url_encode_decode" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "barcode_generator": { "key": "barcode_generator", @@ -1554,7 +1633,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "fake_iban_generator", "list_randomizer" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "fake_iban_generator": { "key": "fake_iban_generator", @@ -1572,7 +1652,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "id_generator", "password_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "list_randomizer": { "key": "list_randomizer", @@ -1590,7 +1671,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "lorem_ipsum", "password_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "ascii_art_generator": { "key": "ascii_art_generator", @@ -1606,7 +1688,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "markdown_preview", "text_diff_checker" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "env_parser": { "key": "env_parser", @@ -1623,7 +1706,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "yaml_json_converter", "base64_encode_decode" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "id_generator": { "key": "id_generator", @@ -1641,7 +1725,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "unix_timestamp", "hash_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "regex_tester": { "key": "regex_tester", @@ -1659,6 +1744,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_encode_decode" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "regex", "regexp", @@ -1689,7 +1775,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "jsonpath_playground", "url_encode_decode" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "crontab_generator": { "key": "crontab_generator", @@ -1706,6 +1793,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "regex_tester" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "cron", "crontab", @@ -1737,7 +1825,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_parser", "chmod_calculator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "cron_visualizer": { "key": "cron_visualizer", @@ -1753,7 +1842,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "unix_timestamp", "regex_tester" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "http_status_codes": { "key": "http_status_codes", @@ -1770,7 +1860,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_parser", "http_request_builder" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "chmod_calculator": { "key": "chmod_calculator", @@ -1787,7 +1878,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "user_agent_parser", "cidr_subnet_calculator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "cidr_subnet_calculator": { "key": "cidr_subnet_calculator", @@ -1805,7 +1897,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "chmod_calculator", "http_status_codes" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "url_parser": { "key": "url_parser", @@ -1823,7 +1916,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "user_agent_parser", "http_request_builder" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "certificate_decoder": { "key": "certificate_decoder", @@ -1841,7 +1935,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_status_codes", "security_header_analyzer" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "http_request_builder": { "key": "http_request_builder", @@ -1859,7 +1954,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_encode_decode", "user_agent_parser" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "curl_to_code": { "key": "curl_to_code", @@ -1877,7 +1973,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_status_codes", "jwt_decoder" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "ndjson_formatter": { "key": "ndjson_formatter", @@ -1895,7 +1992,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_diff_viewer", "jsonpath_playground" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "jwt_verifier": { "key": "jwt_verifier", @@ -1914,7 +2012,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "base64_encode_decode", "certificate_decoder" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "slugify_case_converter": { "key": "slugify_case_converter", @@ -1932,7 +2031,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "hash_generator", "text_diff_checker" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "invisible_chars_detector": { "key": "invisible_chars_detector", @@ -1952,6 +2052,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_encode_decode" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "invisible chars", "zero width", @@ -1980,7 +2081,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "url_parser", "csp_parser" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "csp_parser": { "key": "csp_parser", @@ -1998,7 +2100,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "header_diff", "robots_txt_tester" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "csv_diff": { "key": "csv_diff", @@ -2016,7 +2119,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_diff_viewer", "header_diff" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "header_diff": { "key": "header_diff", @@ -2034,7 +2138,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "csp_parser", "text_diff_checker" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "security_header_analyzer": { "key": "security_header_analyzer", @@ -2052,7 +2157,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_status_codes", "certificate_decoder" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "totp_generator": { "key": "totp_generator", @@ -2070,7 +2176,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "uuid_generator", "id_generator" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "openapi_mock": { "key": "openapi_mock", @@ -2088,7 +2195,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "json_formatter", "curl_to_code" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "docker_run_to_compose": { "key": "docker_run_to_compose", @@ -2107,6 +2215,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "curl_to_code" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "docker", "compose", @@ -2136,6 +2245,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "text_diff_checker" ], "networkAccess": "none", + "persistInput": false, "searchKeywords": [ "parse logs", "analyze logs", @@ -2164,6 +2274,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "yaml_json_converter" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "jq", "json query", @@ -2191,7 +2302,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "jwt_decoder", "text_diff_checker" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "gzip_brotli_lab": { "key": "gzip_brotli_lab", @@ -2209,7 +2321,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_request_builder", "local_log_parser" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "yaml_merge_patch_explorer": { "key": "yaml_merge_patch_explorer", @@ -2227,7 +2340,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "openapi_viewer", "docker_run_to_compose" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "yq_playground": { "key": "yq_playground", @@ -2246,7 +2360,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "jq_playground", "jsonpath_playground" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "structured_data_visualizer": { "key": "structured_data_visualizer", @@ -2265,7 +2380,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "xml_formatter", "jsonpath_playground" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "har_viewer_sanitizer": { "key": "har_viewer_sanitizer", @@ -2284,7 +2400,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "http_status_codes", "security_header_analyzer" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "pipeline_builder": { "key": "pipeline_builder", @@ -2304,6 +2421,7 @@ const CLIENT_TOOL_LOOKUP: Record = { "log_scrubber" ], "networkAccess": "none", + "persistInput": null, "searchKeywords": [ "pipeline", "recipe builder", @@ -2330,7 +2448,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "base64_encode_decode", "security_header_analyzer" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": false }, "asn1_der_inspector": { "key": "asn1_der_inspector", @@ -2348,7 +2467,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "hex_bytes_workbench", "jwt_decoder" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "hex_bytes_workbench": { "key": "hex_bytes_workbench", @@ -2366,7 +2486,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "hash_generator", "unicode_inspector" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null }, "unicode_inspector": { "key": "unicode_inspector", @@ -2384,7 +2505,8 @@ const CLIENT_TOOL_LOOKUP: Record = { "hex_bytes_workbench", "multiple_whitespace_remover" ], - "networkAccess": "none" + "networkAccess": "none", + "persistInput": null } } diff --git a/src/generated/tool-index.json b/src/generated/tool-index.json index b0198571..d388ed57 100644 --- a/src/generated/tool-index.json +++ b/src/generated/tool-index.json @@ -1,5 +1,5 @@ { - "generatedAt": "2026-06-17T14:42:53.061Z", + "generatedAt": "2026-06-17T14:58:43.146Z", "counts": { "canonicalTools": 121, "aliasRoutes": 1, @@ -25,6 +25,7 @@ "format json online" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/json-formatter/manifest.ts" }, @@ -46,6 +47,7 @@ "xml pretty print" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/xml-formatter/manifest.ts" }, @@ -66,6 +68,7 @@ "sql pretty print" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/sql-formatter/manifest.ts" }, @@ -87,6 +90,7 @@ "format javascript online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/javascript-formatter/manifest.ts" }, @@ -108,6 +112,7 @@ "compress javascript online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/javascript-minifier/manifest.ts" }, @@ -129,6 +134,7 @@ "compress html online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/html-minifier/manifest.ts" }, @@ -150,6 +156,7 @@ "html entities decode" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/html-encoder-decoder/manifest.ts" }, @@ -171,6 +178,7 @@ "beautify html css" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/html-css-beautifier/manifest.ts" }, @@ -192,6 +200,7 @@ "html pretty print" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/html-formatter/manifest.ts" }, @@ -213,6 +222,7 @@ "yaml json transform" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/yaml-json-converter/manifest.ts" }, @@ -234,6 +244,7 @@ "gfm preview" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/markdown-preview/manifest.ts" }, @@ -255,6 +266,7 @@ "html2md online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/html-to-markdown/manifest.ts" }, @@ -275,6 +287,7 @@ "json to ts" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/json-to-typescript/manifest.ts" }, @@ -295,6 +308,7 @@ "css optimize" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-minifier/manifest.ts" }, @@ -315,6 +329,7 @@ "compress svg" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/svg-optimizer/manifest.ts" }, @@ -336,6 +351,7 @@ "json path online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/jsonpath-playground/manifest.ts" }, @@ -356,6 +372,7 @@ "api spec viewer" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/openapi-viewer/manifest.ts" }, @@ -376,6 +393,7 @@ "json compare tool" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/json-diff-viewer/manifest.ts" }, @@ -397,6 +415,7 @@ "csv json transform" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/csv-json-converter/manifest.ts" }, @@ -417,6 +436,7 @@ "base64 converter online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/base64-encode-decode/manifest.ts" }, @@ -437,6 +457,7 @@ "url encoder online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/url-encode-decode/manifest.ts" }, @@ -458,6 +479,7 @@ "json web token decoder" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/jwt-decoder/manifest.ts" }, @@ -479,6 +501,7 @@ "jwt all in one tool" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/jwt-workbench/manifest.ts" }, @@ -500,6 +523,7 @@ "sha512 generator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/hash-generator/manifest.ts" }, @@ -521,6 +545,7 @@ "md5 encoder" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/md5-generator/manifest.ts" }, @@ -541,6 +566,7 @@ "text comparison tool" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/text-diff-checker/manifest.ts" }, @@ -562,6 +588,7 @@ "trim text online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/multiple-whitespace-remover/manifest.ts" }, @@ -583,6 +610,7 @@ "text analyzer" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/letter-counter/manifest.ts" }, @@ -604,6 +632,7 @@ "focus reading tool" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/bionic-reading-converter/manifest.ts" }, @@ -625,6 +654,7 @@ "font combination tool" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/google-fonts-pair-finder/manifest.ts" }, @@ -646,6 +676,7 @@ "handwriting png" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/text-to-handwriting-converter/manifest.ts" }, @@ -667,6 +698,7 @@ "code image generator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/code-to-image-converter/manifest.ts" }, @@ -686,6 +718,7 @@ "convert image base64 online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/image-base64/manifest.ts" }, @@ -706,6 +739,7 @@ "unix time online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/unix-timestamp/manifest.ts" }, @@ -726,6 +760,7 @@ "guid generator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/uuid-generator/manifest.ts" }, @@ -745,6 +780,7 @@ "placeholder text" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/lorem-ipsum/manifest.ts" }, @@ -764,6 +800,7 @@ "strong password generator online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/password-generator/manifest.ts" }, @@ -784,6 +821,7 @@ "color picker online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/color-converter/manifest.ts" }, @@ -805,6 +843,7 @@ "react native style shadow" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/react-native-shadow-generator/manifest.ts" }, @@ -826,6 +865,7 @@ "palette maker online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/ai-color-palette-generator/manifest.ts" }, @@ -847,6 +887,7 @@ "hsl color blend" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/color-mixer/manifest.ts" }, @@ -868,6 +909,7 @@ "generate color tokens" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/color-shades-generator/manifest.ts" }, @@ -889,6 +931,7 @@ "image color analyzer" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/image-average-color-finder/manifest.ts" }, @@ -910,6 +953,7 @@ "image quote generator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/image-caption-generator/manifest.ts" }, @@ -931,6 +975,7 @@ "dominant colors from photo" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/image-color-extractor/manifest.ts" }, @@ -952,6 +997,7 @@ "sample image color" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/image-color-picker/manifest.ts" }, @@ -973,6 +1019,7 @@ "crop png jpg" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/image-cropper/manifest.ts" }, @@ -994,6 +1041,7 @@ "image effects online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/image-filters/manifest.ts" }, @@ -1015,6 +1063,7 @@ "photo preset filters" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/instagram-filters/manifest.ts" }, @@ -1036,6 +1085,7 @@ "instagram post template" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/instagram-post-generator/manifest.ts" }, @@ -1057,6 +1107,7 @@ "social story maker" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/instagram-story-generator/manifest.ts" }, @@ -1078,6 +1129,7 @@ "social preview meta" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/open-graph-meta-generator/manifest.ts" }, @@ -1099,6 +1151,7 @@ "social tweet mockup" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/tweet-generator/manifest.ts" }, @@ -1120,6 +1173,7 @@ "x post image maker" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/tweet-to-image-converter/manifest.ts" }, @@ -1141,6 +1195,7 @@ "cpm ctr calculator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/twitter-ad-revenue-generator/manifest.ts" }, @@ -1162,6 +1217,7 @@ "authorized ig image download" ], "networkAccess": "user_requested", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/instagram-photo-downloader/manifest.ts" }, @@ -1183,6 +1239,7 @@ "get vimeo video thumbnail" ], "networkAccess": "user_requested", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/vimeo-thumbnail-grabber/manifest.ts" }, @@ -1204,6 +1261,7 @@ "get youtube thumbnail url" ], "networkAccess": "user_requested", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/youtube-thumbnail-grabber/manifest.ts" }, @@ -1225,6 +1283,7 @@ "image resize tool" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/image-resizer/manifest.ts" }, @@ -1246,6 +1305,7 @@ "hide sensitive info image" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/photo-censor/manifest.ts" }, @@ -1267,6 +1327,7 @@ "multi page pdf exporter" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/scanned-pdf-converter/manifest.ts" }, @@ -1288,6 +1349,7 @@ "random blob svg" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/svg-blob-generator/manifest.ts" }, @@ -1309,6 +1371,7 @@ "svg texture generator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/svg-pattern-generator/manifest.ts" }, @@ -1330,6 +1393,7 @@ "svg path converter" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/svg-stroke-to-fill-converter/manifest.ts" }, @@ -1351,6 +1415,7 @@ "export svg as png" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/svg-to-png-converter/manifest.ts" }, @@ -1372,6 +1437,7 @@ "pattern css online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-background-pattern-generator/manifest.ts" }, @@ -1393,6 +1459,7 @@ "radius generator online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-border-radius-generator/manifest.ts" }, @@ -1414,6 +1481,7 @@ "box shadow css online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-box-shadow-generator/manifest.ts" }, @@ -1435,6 +1503,7 @@ "checkbox ui generator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-checkbox-generator/manifest.ts" }, @@ -1456,6 +1525,7 @@ "shape generator css" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-clip-path-generator/manifest.ts" }, @@ -1477,6 +1547,7 @@ "cubic bezier tool" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-cubic-bezier-generator/manifest.ts" }, @@ -1498,6 +1569,7 @@ "backdrop filter generator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-glassmorphism-generator/manifest.ts" }, @@ -1519,6 +1591,7 @@ "gradient maker online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-gradient-generator/manifest.ts" }, @@ -1540,6 +1613,7 @@ "css preloaders" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-loader-generator/manifest.ts" }, @@ -1561,6 +1635,7 @@ "switch ui generator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-switch-generator/manifest.ts" }, @@ -1582,6 +1657,7 @@ "animated text effect css" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-text-glitch-effect-generator/manifest.ts" }, @@ -1603,6 +1679,7 @@ "directional triangle css" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/css-triangle-generator/manifest.ts" }, @@ -1622,6 +1699,7 @@ "qr code maker" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/qr-code-generator/manifest.ts" }, @@ -1643,6 +1721,7 @@ "barcode png svg export" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/barcode-generator/manifest.ts" }, @@ -1664,6 +1743,7 @@ "dummy bank iban" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/fake-iban-generator/manifest.ts" }, @@ -1685,6 +1765,7 @@ "seeded random list" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/list-randomizer/manifest.ts" }, @@ -1704,6 +1785,7 @@ "figlet online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/ascii-art-generator/manifest.ts" }, @@ -1724,6 +1806,7 @@ "environment variable viewer" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/env-parser/manifest.ts" }, @@ -1745,6 +1828,7 @@ "unique id generator online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/id-generator/manifest.ts" }, @@ -1765,6 +1849,7 @@ "regex debugger" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/regex-tester/manifest.ts" }, @@ -1786,6 +1871,7 @@ "regular expression generator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/regex-generator/manifest.ts" }, @@ -1805,6 +1891,7 @@ "cron maker online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/crontab-generator/manifest.ts" }, @@ -1825,6 +1912,7 @@ "ua string analyzer online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/user-agent-parser/manifest.ts" }, @@ -1844,6 +1932,7 @@ "visualize cron expression" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/cron-visualizer/manifest.ts" }, @@ -1864,6 +1953,7 @@ "status code reference" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/http-status-codes/manifest.ts" }, @@ -1884,6 +1974,7 @@ "file permissions chmod" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/chmod-calculator/manifest.ts" }, @@ -1905,6 +1996,7 @@ "network broadcast calculator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/cidr-subnet-calculator/manifest.ts" }, @@ -1926,6 +2018,7 @@ "url components breakdown" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/url-parser/manifest.ts" }, @@ -1947,6 +2040,7 @@ "certificate parser" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/certificate-decoder/manifest.ts" }, @@ -1968,6 +2062,7 @@ "rest client online" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/http-request-builder/manifest.ts" }, @@ -1989,6 +2084,7 @@ "curl converter online" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/curl-to-code/manifest.ts" }, @@ -2010,6 +2106,7 @@ "ndjson to json array" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/ndjson-formatter/manifest.ts" }, @@ -2032,6 +2129,7 @@ "jwt validator online" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/jwt-verifier/manifest.ts" }, @@ -2053,6 +2151,7 @@ "snake case to camel case" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/slugify-case-converter/manifest.ts" }, @@ -2075,6 +2174,7 @@ "hidden characters finder" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/invisible-characters-detector/manifest.ts" }, @@ -2096,6 +2196,7 @@ "crawler access tester" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/robots-txt-tester/manifest.ts" }, @@ -2117,6 +2218,7 @@ "csp checker" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/csp-parser/manifest.ts" }, @@ -2138,6 +2240,7 @@ "spreadsheet diff" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/csv-diff/manifest.ts" }, @@ -2159,6 +2262,7 @@ "header comparison" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/header-diff/manifest.ts" }, @@ -2180,6 +2284,7 @@ "header security audit" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/security-header-analyzer/manifest.ts" }, @@ -2201,6 +2306,7 @@ "authenticator code generator" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/totp-generator/manifest.ts" }, @@ -2222,6 +2328,7 @@ "mock response generator" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/openapi-mock/manifest.ts" }, @@ -2243,6 +2350,7 @@ "containerize" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/docker-run-to-compose/manifest.ts" }, @@ -2265,6 +2373,7 @@ "log viewer" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/local-log-parser/manifest.ts" }, @@ -2287,6 +2396,7 @@ "json transform" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/jq-playground/manifest.ts" }, @@ -2308,6 +2418,7 @@ "pii remover" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/log-scrubber/manifest.ts" }, @@ -2329,6 +2440,7 @@ "http compression" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/gzip-brotli-lab/manifest.ts" }, @@ -2350,6 +2462,7 @@ "kubernetes yaml" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/yaml-merge-patch-explorer/manifest.ts" }, @@ -2372,6 +2485,7 @@ "yaml transform" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/yq-playground/manifest.ts" }, @@ -2394,6 +2508,7 @@ "data graph" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/structured-data-visualizer/manifest.ts" }, @@ -2416,6 +2531,7 @@ "har redaction" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/har-viewer-sanitizer/manifest.ts" }, @@ -2438,6 +2554,7 @@ "developer workbench" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/pipeline-builder/manifest.ts" }, @@ -2459,6 +2576,7 @@ "saml assertion viewer" ], "networkAccess": "none", + "persistInput": false, "updatedAt": null, "sourceFile": "src/features/tools/saml-decoder/manifest.ts" }, @@ -2480,6 +2598,7 @@ "certificate der decoder" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/asn1-der-inspector/manifest.ts" }, @@ -2501,6 +2620,7 @@ "utf8 hex converter" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/hex-bytes-workbench/manifest.ts" }, @@ -2522,6 +2642,7 @@ "utf8 inspector" ], "networkAccess": "none", + "persistInput": null, "updatedAt": null, "sourceFile": "src/features/tools/unicode-inspector/manifest.ts" } diff --git a/tests/guards/sensitive-tool-persistence-policy.test.ts b/tests/guards/sensitive-tool-persistence-policy.test.ts new file mode 100644 index 00000000..312930b4 --- /dev/null +++ b/tests/guards/sensitive-tool-persistence-policy.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from "vitest" +import { TOOL_MANIFESTS } from "@/core/registry" +import type { ToolMeta } from "@/core/registry/types" + +const SENSITIVE_TOOL_KEYS = [ + "json_formatter", + "jwt_decoder", + "jwt_workbench", + "jwt_verifier", + "saml_decoder", + "certificate_decoder", + "har_viewer_sanitizer", + "log_scrubber", + "security_header_analyzer", + "totp_generator", + "env_parser", + "local_log_parser", + "header_diff", + "http_request_builder", +] + +describe("sensitive tool persistence policy", () => { + it("marks sensitive payload tools as non-persistent by default", () => { + const manifests = new Map((TOOL_MANIFESTS as readonly ToolMeta[]).map((tool) => [tool.key, tool])) + const offenders = SENSITIVE_TOOL_KEYS.flatMap((key) => { + const tool = manifests.get(key) + if (!tool) return [`${key}: missing manifest`] + return tool.persistInput === false ? [] : [`${key}: persistInput must be false`] + }) + + expect(offenders).toEqual([]) + }) +}) From f9e784c9963ce1ab05622e146ddfd32c3ce8307b Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 08:03:17 -0700 Subject: [PATCH 07/28] feat(security): show external network notices --- src/components/layout/route-page-chrome.tsx | 6 +- .../tool-shell/external-network-notice.tsx | 26 +++++++ tests/component/route-page-chrome.test.tsx | 71 +++++++++++++++++++ 3 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/features/tool-shell/external-network-notice.tsx create mode 100644 tests/component/route-page-chrome.test.tsx diff --git a/src/components/layout/route-page-chrome.tsx b/src/components/layout/route-page-chrome.tsx index 6737a385..911393f3 100644 --- a/src/components/layout/route-page-chrome.tsx +++ b/src/components/layout/route-page-chrome.tsx @@ -11,6 +11,7 @@ import { getRouteIntentCopy } from "@/core/seo/route-intent-copy" import { getRouteContext } from "@/core/routing/route-context" import { recordRecentToolKey } from "@/core/storage/tool-discovery-state" import { getClientToolBySlug } from "@/generated/client-tool-lookup" +import { ExternalNetworkNotice } from "@/features/tool-shell/external-network-notice" const EXCLUDED_CONTENT_INTRO_SLUGS = new Set(["about", "pricing", "contact", "privacy", "terms", "install-app"]) @@ -27,7 +28,7 @@ function RoutePageChromeContent({ children, pathname }: RoutePageChromeProps) { if (routeContext.routeType !== "tool" || !routeContext.slug) return null const tool = getClientToolBySlug(routeContext.slug) if (!tool) return null - return { key: tool.key, slug: tool.slug } + return { key: tool.key, slug: tool.slug, networkAccess: tool.networkAccess } }, [routeContext]) useEffect(() => { @@ -62,6 +63,9 @@ function RoutePageChromeContent({ children, pathname }: RoutePageChromeProps) { {routeIntentCopy} ) : null} + {activeTool?.networkAccess && activeTool.networkAccess !== "none" ? ( + + ) : null} {children} {routeContext.routeType === "tool" ? (
diff --git a/src/features/tool-shell/external-network-notice.tsx b/src/features/tool-shell/external-network-notice.tsx new file mode 100644 index 00000000..54463e8f --- /dev/null +++ b/src/features/tool-shell/external-network-notice.tsx @@ -0,0 +1,26 @@ +import { ExternalLink } from "lucide-react" +import type { ToolNetworkAccess } from "@/core/registry/types" + +type ExternalNetworkNoticeProps = { + networkAccess: ToolNetworkAccess +} + +export function ExternalNetworkNotice({ networkAccess }: ExternalNetworkNoticeProps) { + if (networkAccess === "none") return null + + const message = networkAccess === "third_party_api" + ? "This tool may contact a third-party service from your browser as part of its workflow." + : "This tool may request or open a URL you provide from your browser when you explicitly run that action." + + return ( +
+
+ +
+

External network notice

+

{message}

+
+
+
+ ) +} diff --git a/tests/component/route-page-chrome.test.tsx b/tests/component/route-page-chrome.test.tsx new file mode 100644 index 00000000..89f8736f --- /dev/null +++ b/tests/component/route-page-chrome.test.tsx @@ -0,0 +1,71 @@ +import { render, screen } from "@testing-library/react" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { RoutePageChrome } from "@/components/layout/route-page-chrome" + +const mocks = vi.hoisted(() => { + const translations = { + common: { + install_guide: "Install guide", + install_inline_description: "Install description", + install_inline_title: "Install Byteflow", + }, + } + + return { + langValue: { + lang: "en", + t: translations, + }, + translations, + } +}) + +vi.mock("next/link", () => ({ + default: ({ href, children, ...props }: { href: string; children: React.ReactNode }) => ( + + {children} + + ), +})) + +vi.mock("@/core/i18n/lang-provider", () => ({ + useLang: () => mocks.langValue, +})) + +vi.mock("@/core/storage/tool-discovery-state", () => ({ + recordRecentToolKey: vi.fn(), +})) + +vi.mock("@/core/seo/components/related-tools", () => ({ + RelatedTools: ({ toolKey }: { toolKey: string }) =>
{toolKey}
, +})) + +describe("RoutePageChrome", () => { + beforeEach(() => { + mocks.langValue = { + lang: "en", + t: mocks.translations, + } + }) + + it("shows an external network notice for user-requested network tools", () => { + render( + +
tool body
+
, + ) + + expect(screen.getByText("External network notice")).toBeInTheDocument() + expect(screen.getByText(/request or open a URL you provide/i)).toBeInTheDocument() + }) + + it("does not show an external network notice for local-only tools", () => { + render( + +
tool body
+
, + ) + + expect(screen.queryByText("External network notice")).not.toBeInTheDocument() + }) +}) From 60ae25f22215c4b3ddc68c9a44df65e1309f8663 Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 08:05:41 -0700 Subject: [PATCH 08/28] test(security): guard html injection surfaces --- .../html-injection-surface-guard.test.ts | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/guards/html-injection-surface-guard.test.ts diff --git a/tests/guards/html-injection-surface-guard.test.ts b/tests/guards/html-injection-surface-guard.test.ts new file mode 100644 index 00000000..e8394c6b --- /dev/null +++ b/tests/guards/html-injection-surface-guard.test.ts @@ -0,0 +1,85 @@ +import fs from "node:fs" +import path from "node:path" +import { describe, expect, it } from "vitest" + +const SRC_ROOT = path.join(process.cwd(), "src") +const TEXT_FILE_PATTERN = /\.(ts|tsx|js|jsx)$/ +const DANGEROUS_HTML_CALL = "dangerouslySetInnerHTML" + +const ALLOWED_DANGEROUS_HTML_FILES = [ + "src/app/layout.tsx", + "src/app/page.tsx", + "src/core/seo/components/json-ld-script.tsx", + "src/core/seo/components/legacy-tool-redirect-page.tsx", + "src/features/tools/svg-optimizer/page.tsx", +] + +function walk(dir: string): string[] { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + const files: string[] = [] + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + files.push(...walk(fullPath)) + continue + } + if (entry.isFile() && TEXT_FILE_PATTERN.test(entry.name)) { + files.push(fullPath) + } + } + + return files +} + +function readSource(relativePath: string): string { + return fs.readFileSync(path.join(process.cwd(), relativePath), "utf8") +} + +function countMatches(source: string, pattern: RegExp): number { + return Array.from(source.matchAll(pattern)).length +} + +describe("HTML injection surface guard", () => { + it("keeps React HTML injection callsites on an explicit security-reviewed allowlist", () => { + const hits = walk(SRC_ROOT) + .filter((file) => fs.readFileSync(file, "utf8").includes(DANGEROUS_HTML_CALL)) + .map((file) => path.relative(process.cwd(), file).replace(/\\/g, "/")) + .sort() + + expect(hits).toEqual([...ALLOWED_DANGEROUS_HTML_FILES].sort()) + }) + + it("routes JSON-LD through the shared serializer that escapes script-breaking characters", () => { + const source = readSource("src/core/seo/components/json-ld-script.tsx") + + expect(source).toContain("export function serializeJsonLd(jsonLd: unknown): string") + expect(source).toContain("JSON.stringify(jsonLd).replace(/ { + const source = readSource("src/features/tools/svg-optimizer/page.tsx") + + expect(source).toContain("import DOMPurify from \"isomorphic-dompurify\"") + expect(source).toContain("dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(output) }}") + expect(countMatches(source, /dangerouslySetInnerHTML=\{\{/g)).toBe(1) + }) + + it("embeds dynamic redirect and runtime script data with JSON serialization only", () => { + const rootPageSource = readSource("src/app/page.tsx") + const legacyRedirectSource = readSource("src/core/seo/components/legacy-tool-redirect-page.tsx") + const layoutSource = readSource("src/app/layout.tsx") + + expect(rootPageSource).toContain("var supported = ${JSON.stringify(LOCALES)};") + expect(countMatches(rootPageSource, /dangerouslySetInnerHTML=\{\{/g)).toBe(1) + + expect(legacyRedirectSource).toContain("const escapedHref = JSON.stringify(href)") + expect(legacyRedirectSource).toContain("var target = ${escapedHref};") + expect(countMatches(legacyRedirectSource, /dangerouslySetInnerHTML=\{\{/g)).toBe(1) + + expect(layoutSource).toContain("var locales = ${JSON.stringify(LOCALES)};") + expect(countMatches(layoutSource, /dangerouslySetInnerHTML=\{\{/g)).toBe(1) + }) +}) From 2b559fdc811eb2cd87a24142fe1a597d5b20b46a Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 08:10:19 -0700 Subject: [PATCH 09/28] refactor(json-formatter): extract page UI modules --- .../json-formatter/json-import-dropzone.tsx | 65 ++++++ .../json-formatter/json-output-toolbar.tsx | 106 +++++++++ .../tools/json-formatter/json-tree-search.tsx | 48 ++++ src/features/tools/json-formatter/page.tsx | 208 +++++------------- .../feature-tool-module-boundaries.test.ts | 2 +- 5 files changed, 274 insertions(+), 155 deletions(-) create mode 100644 src/features/tools/json-formatter/json-import-dropzone.tsx create mode 100644 src/features/tools/json-formatter/json-output-toolbar.tsx create mode 100644 src/features/tools/json-formatter/json-tree-search.tsx diff --git a/src/features/tools/json-formatter/json-import-dropzone.tsx b/src/features/tools/json-formatter/json-import-dropzone.tsx new file mode 100644 index 00000000..960de49f --- /dev/null +++ b/src/features/tools/json-formatter/json-import-dropzone.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { Upload } from "lucide-react" +import { Button } from "@/components/ui/button" +import { TEXT_FILE_IMPORT_ACCEPT } from "@/core/files/text-file-import" + +type JsonImportDropzoneProps = { + fileInputRef: React.RefObject + isDragActive: boolean + onDragActiveChange: (isActive: boolean) => void + onImportFile: (file: File) => void | Promise + onOpenImportPicker: () => void + text: (key: string) => string +} + +export function JsonImportDropzone({ + fileInputRef, + isDragActive, + onDragActiveChange, + onImportFile, + onOpenImportPicker, + text, +}: JsonImportDropzoneProps) { + return ( +
{ + event.preventDefault() + onDragActiveChange(true) + }} + onDragLeave={(event) => { + event.preventDefault() + onDragActiveChange(false) + }} + onDrop={(event) => { + event.preventDefault() + onDragActiveChange(false) + const file = event.dataTransfer.files?.[0] + if (!file) return + void onImportFile(file) + }} + > +
+

+ {text("drag_drop_import_hint")} +

+ +
+ { + const file = event.target.files?.[0] + event.currentTarget.value = "" + if (!file) return + void onImportFile(file) + }} + /> +
+ ) +} diff --git a/src/features/tools/json-formatter/json-output-toolbar.tsx b/src/features/tools/json-formatter/json-output-toolbar.tsx new file mode 100644 index 00000000..0feac107 --- /dev/null +++ b/src/features/tools/json-formatter/json-output-toolbar.tsx @@ -0,0 +1,106 @@ +import { Copy, ListTree, Maximize2, Minimize2, Search } from "lucide-react" +import { Button } from "@/components/ui/button" +import type { ViewMode } from "./types" + +type JsonOutputToolbarProps = { + canCopy: boolean + hasTreeData: boolean + isSearchOpen: boolean + labels: { + collapseAll: string + copyOutput: string + expandAll: string + output: string + search: string + viewText: string + viewTree: string + } + onCollapseAll: () => void + onCopy: () => void + onExpandAll: () => void + onToggleSearch: () => void + onViewModeChange: (mode: ViewMode) => void + viewMode: ViewMode +} + +export function JsonOutputToolbar({ + canCopy, + hasTreeData, + isSearchOpen, + labels, + onCollapseAll, + onCopy, + onExpandAll, + onToggleSearch, + onViewModeChange, + viewMode, +}: JsonOutputToolbarProps) { + return ( +
+
+ {labels.output} +
+ + +
+
+ +
+ {viewMode === "tree" ? ( +
+ + + +
+ ) : null} + + +
+
+ ) +} diff --git a/src/features/tools/json-formatter/json-tree-search.tsx b/src/features/tools/json-formatter/json-tree-search.tsx new file mode 100644 index 00000000..d44eac43 --- /dev/null +++ b/src/features/tools/json-formatter/json-tree-search.tsx @@ -0,0 +1,48 @@ +import { Search, X } from "lucide-react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" + +type JsonTreeSearchProps = { + closeLabel: string + onClose: () => void + onQueryChange: (query: string) => void + placeholder: string + query: string +} + +export function JsonTreeSearch({ + closeLabel, + onClose, + onQueryChange, + placeholder, + query, +}: JsonTreeSearchProps) { + return ( +
+
+ + onQueryChange(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Escape") { + onClose() + } + }} + /> +
+ +
+ ) +} diff --git a/src/features/tools/json-formatter/page.tsx b/src/features/tools/json-formatter/page.tsx index 38ea9f6a..c6b58e7d 100644 --- a/src/features/tools/json-formatter/page.tsx +++ b/src/features/tools/json-formatter/page.tsx @@ -2,18 +2,13 @@ import * as React from "react" import { - Play, - Copy, - Eraser, - Braces, - AlignLeft, - ListTree, - Upload, - ArrowRightLeft, - Maximize2, - Minimize2, - Search, - X, + Play, + Eraser, + Braces, + AlignLeft, + ListTree, + Upload, + ArrowRightLeft, TestTube2, Workflow, } from "lucide-react" @@ -27,15 +22,18 @@ import { MonacoEditor } from "@/features/tool-shell/monaco-editors" import { ToolActionBar, type ToolAction } from "@/features/tool-shell/tool-action-bar" import { ToolEmptyState } from "@/features/tool-shell/tool-empty-state" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" -import { Input } from "@/components/ui/input" +import { Input } from "@/components/ui/input" import { readStorageString, writeStorageString, removeStorageKey } from "@/core/storage/tool-persistence" import { enforceToolInputPersistencePolicy } from "@/core/storage/tool-persistence-policy" -import { importTextFile, TEXT_FILE_IMPORT_ACCEPT } from "@/core/files/text-file-import" -import { buildToolHandoffLink, getToolHandoffFromSearchParams } from "@/core/routing/tool-handoff" -import { safeClipboardWrite } from "@/core/clipboard/clipboard" +import { importTextFile } from "@/core/files/text-file-import" +import { buildToolHandoffLink, getToolHandoffFromSearchParams } from "@/core/routing/tool-handoff" +import { safeClipboardWrite } from "@/core/clipboard/clipboard" import { buildJsonParseErrorMessage } from "@/features/tools/json-formatter/error-utils" import { PrivacyBadge } from "@/features/tool-shell/privacy-badge" import { PrivacyFAQ } from "@/features/tool-shell/privacy-faq" +import { JsonImportDropzone } from "./json-import-dropzone" +import { JsonOutputToolbar } from "./json-output-toolbar" +import { JsonTreeSearch } from "./json-tree-search" import { INPUT_STORAGE_DEBOUNCE_MS, INPUT_STORAGE_KEY, JSON_FORMATTER_PERSISTENCE_POLICY, VIEW_MODE_STORAGE_KEY } from "./constants" import { findMatchingPaths, @@ -575,46 +573,14 @@ export function JsonFormatterPage() { -
{ - event.preventDefault() - setIsImportDragActive(true) - }} - onDragLeave={(event) => { - event.preventDefault() - setIsImportDragActive(false) - }} - onDrop={(event) => { - event.preventDefault() - setIsImportDragActive(false) - const file = event.dataTransfer.files?.[0] - if (!file) return - void handleImportFile(file) - }} - > -
-

- {text("drag_drop_import_hint")} -

- -
- { - const file = event.target.files?.[0] - event.currentTarget.value = "" - if (!file) return - void handleImportFile(file) - }} - /> -
+ {error ? (
@@ -649,72 +615,26 @@ export function JsonFormatterPage() {
-
-
- {t.common.output} -
- - -
-
- -
- {viewMode === "tree" && ( -
- - - -
- )} - - -
-
+ setIsSearchOpen(!isSearchOpen)} + onViewModeChange={switchViewMode} + viewMode={viewMode} + /> {viewMode === "text" ? (
@@ -742,38 +662,18 @@ export function JsonFormatterPage() { {toolT.tree_hint}
- {isSearchOpen && ( -
-
- - setSearchQuery(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Escape") { - setIsSearchOpen(false) - setSearchQuery("") - } - }} - /> -
- -
- )} + {isSearchOpen ? ( + { + setIsSearchOpen(false) + setSearchQuery("") + }} + onQueryChange={setSearchQuery} + placeholder={text("tree_search_placeholder")} + query={searchQuery} + /> + ) : null} {treeData === null ? ( = { - "json-formatter": 850, + "json-formatter": 720, "pipeline-builder": 700, "password-generator": 500, "qr-code-generator": 450, From a52c45042b9c1090b2374df4cd6ad98e70bbb06f Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 08:11:56 -0700 Subject: [PATCH 10/28] test(json-formatter): cover tree path mutations --- tests/unit/json-formatter-tree-logic.test.ts | 100 +++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/unit/json-formatter-tree-logic.test.ts diff --git a/tests/unit/json-formatter-tree-logic.test.ts b/tests/unit/json-formatter-tree-logic.test.ts new file mode 100644 index 00000000..6d5a3876 --- /dev/null +++ b/tests/unit/json-formatter-tree-logic.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest" +import { + findMatchingPaths, + getAllPaths, + getValueAtPath, + pathKey, + removeValueAtPath, + renameObjectKey, + updateValueAtPath, +} from "@/features/tools/json-formatter/logic" +import type { JsonValue } from "@/features/tools/json-formatter/types" + +const sample: JsonValue = { + user: { + name: "Alice", + roles: ["admin", "editor"], + profile: { + active: true, + }, + }, + count: 2, +} + +describe("json formatter tree logic", () => { + it("builds stable path keys for root and nested nodes", () => { + expect(pathKey([])).toBe("$") + expect(pathKey(["user", "roles", 1])).toBe("user__roles__1") + }) + + it("reads nested object and array values by path", () => { + expect(getValueAtPath(sample, ["user", "name"])).toBe("Alice") + expect(getValueAtPath(sample, ["user", "roles", 1])).toBe("editor") + }) + + it("returns the root value when a path cannot be traversed", () => { + expect(getValueAtPath(sample, ["user", 0])).toBe(sample) + expect(getValueAtPath(sample, ["count", "nested"])).toBe(sample) + }) + + it("updates nested values immutably", () => { + const next = updateValueAtPath(sample, ["user", "roles", 0], "owner") + + expect(getValueAtPath(next, ["user", "roles", 0])).toBe("owner") + expect(getValueAtPath(sample, ["user", "roles", 0])).toBe("admin") + expect(next).not.toBe(sample) + }) + + it("removes object keys and array entries by path", () => { + const withoutProfile = removeValueAtPath(sample, ["user", "profile"]) + const withoutRole = removeValueAtPath(sample, ["user", "roles", 0]) + + expect(getValueAtPath(withoutProfile, ["user"])).toEqual({ + name: "Alice", + roles: ["admin", "editor"], + }) + expect(getValueAtPath(withoutRole, ["user", "roles"])).toEqual(["editor"]) + }) + + it("renames object keys without overwriting existing keys", () => { + const renamed = renameObjectKey(sample, ["user"], "name", "displayName") + const blocked = renameObjectKey(sample, ["user"], "name", "roles") + + expect(getValueAtPath(renamed, ["user"])).toEqual({ + displayName: "Alice", + roles: ["admin", "editor"], + profile: { + active: true, + }, + }) + expect(blocked).toBe(sample) + }) + + it("collects all tree paths for expand-all behavior", () => { + expect([...getAllPaths(sample)].sort()).toEqual([ + "$", + "count", + "user", + "user__name", + "user__profile", + "user__profile__active", + "user__roles", + "user__roles__0", + "user__roles__1", + ]) + }) + + it("finds matching value paths and includes parent paths for auto-expand", () => { + const result = findMatchingPaths(sample, "editor") + + expect([...result.matched].sort()).toEqual(["user__roles", "user__roles__1"]) + expect([...result.parents].sort()).toEqual(["$", "user", "user__roles", "user__roles__1"]) + }) + + it("finds matching key paths case-insensitively", () => { + const result = findMatchingPaths(sample, "PROFILE") + + expect(result.matched.has("user__profile")).toBe(true) + expect(result.parents.has("user")).toBe(true) + }) +}) From a9a1ba2e4141cda025dfd7bb3e3f3f3fa47b748e Mon Sep 17 00:00:00 2001 From: baixiangcpp Date: Wed, 17 Jun 2026 08:16:11 -0700 Subject: [PATCH 11/28] refactor(pipeline-builder): extract page panels --- src/features/tools/pipeline-builder/page.tsx | 227 +++--------------- .../pipeline-builder/pipeline-run-log.tsx | 47 ++++ .../pipeline-saved-recipes.tsx | 56 +++++ .../pipeline-builder/pipeline-step-list.tsx | 128 ++++++++++ .../pipeline-template-list.tsx | 44 ++++ .../feature-tool-module-boundaries.test.ts | 2 +- 6 files changed, 314 insertions(+), 190 deletions(-) create mode 100644 src/features/tools/pipeline-builder/pipeline-run-log.tsx create mode 100644 src/features/tools/pipeline-builder/pipeline-saved-recipes.tsx create mode 100644 src/features/tools/pipeline-builder/pipeline-step-list.tsx create mode 100644 src/features/tools/pipeline-builder/pipeline-template-list.tsx diff --git a/src/features/tools/pipeline-builder/page.tsx b/src/features/tools/pipeline-builder/page.tsx index 5ee6db38..51b062f0 100644 --- a/src/features/tools/pipeline-builder/page.tsx +++ b/src/features/tools/pipeline-builder/page.tsx @@ -2,18 +2,12 @@ import * as React from "react" import { - ArrowDown, - ArrowUp, - BookOpen, Copy, Download, FileInput, - FolderOpen, Link2, Play, - Plus, Save, - Trash2, Workflow, } from "lucide-react" import { toast } from "sonner" @@ -43,6 +37,10 @@ import { StepOptions } from "./components" import { downloadText } from "./browser-actions" import { FIRST_ADAPTER, SHARE_PARAM } from "./constants" import { createId, createRecipe, createStep, updateRecipeTimestamp } from "./logic" +import { PipelineRunLog } from "./pipeline-run-log" +import { PipelineSavedRecipes } from "./pipeline-saved-recipes" +import { PipelineStepList } from "./pipeline-step-list" +import { PipelineTemplateList } from "./pipeline-template-list" import type { OptionValue } from "./types" export function PipelineBuilderPage() { @@ -347,153 +345,39 @@ export function PipelineBuilderPage() {
-
-
- -
-

{text("templates_title")}

-

{text("templates_description")}

-
-
-
- {PIPELINE_RECIPE_TEMPLATES.map((template) => ( -
-
-
-

{text(template.titleKey)}

-

{text(template.descriptionKey)}

-
- -
-
- ))} -
-
- -
-
-

{text("steps_title")}

- {recipe.steps.length}/{recipe.settings.maxSteps} -
-
- - -
-
- {recipe.steps.length === 0 ? ( -
{text("no_steps")}
- ) : recipe.steps.map((step, index) => { - const adapterTitle = (t.tools[step.toolKey] as Record | undefined)?.title ?? step.toolKey - const active = step.id === selectedStep?.id - return ( -
- -
- - - -
-
- ) - })} -
-
- -
-

{text("saved_recipes")}

-
- -
-
- - -
- {storageMessage ?

{storageMessage}

: null} -
+ + + ({ + title: (t.tools[adapter.toolKey] as Record | undefined)?.title ?? adapter.toolKey, + toolKey: adapter.toolKey, + }))} + maxSteps={recipe.settings.maxSteps} + onAddStep={addStep} + onMoveStep={moveStep} + onPendingToolKeyChange={setPendingToolKey} + onRemoveStep={removeStep} + onSelectStep={setSelectedStepId} + pendingToolKey={pendingToolKey} + selectedStepId={selectedStep?.id ?? null} + steps={recipe.steps} + text={text} + /> + + void deleteRecipe()} + onLoadRecipe={() => void loadRecipe()} + onSelectedSavedIdChange={setSelectedSavedId} + savedRecipes={savedRecipes} + selectedSavedId={selectedSavedId} + storageAvailable={storageAvailable} + storageMessage={storageMessage} + text={text} + />
@@ -529,42 +413,7 @@ export function PipelineBuilderPage() { /> -
-

{text("run_log")}

-
- {result ? ( - - - - - - - - - - - {result.steps.map((step) => ( - - - - - - - ))} - -
{text("table_step")}{text("table_status")}{text("table_bytes")}{text("table_message")}
{step.stepId}{step.ok ? text("status_ok") : text("status_failed")}{step.inputBytes} {"->"} {step.outputBytes} - {step.error?.message || step.warnings.join("; ") || text("no_message")} -
- ) : ( -
{text("run_log_empty")}
- )} -
- {result?.errors.length ? ( -
- {result.errors.join("\n")} -
- ) : null} -
+