Utilo

I stopped pasting JWTs into random websites, so I wrote client-side converters instead

Why Utilo's converters run in the tab: no application backend, how the type inference and the curl and cron parsers actually behave, and what the static site still cannot promise.

· 11 min read

I had a production token in my clipboard and a page open that said to paste a JWT into its box. The header and the payload are only Base64, so the tool did not need a signing key to show the claims. It needed the token, which is the secret as far as a request log is concerned. I closed the tab. I still wanted the claims on a machine I control, without a server that would be tempted to store the body.

The converters are ordinary pages, and the work happens after they load. This note is about being precise: a JSON sample into types, a curl command into Python, and a cron line into a sentence and the next runs. The tools are linked where the behavior is specific. This page is not the tool.

The paste I would not make

A JWT is three Base64URL segments. The first is a JSON header, the second is a JSON payload, and the third is a signature over the first two. Anyone who can see the token can read the first two. That is the format, not a bug in a decoder. The JWT decoder splits on the dots, decodes those two segments, and pretty-prints the JSON. If the payload has a numeric exp, iat, or nbf, it prints that instant beside the clock in the browser. A past exp is marked expired. The comparison is payload.exp * 1000 against Date.now(). It is not a signature check.

The signature panel prints the third segment and stops. There is no key field, no JWKS fetch, and no badge that would imply HS256 or RS256 had been verified. A green clock means the expiry claim is still in the future. It does not mean a party you trust issued the token. The longer account of the three segments is the guide to how a JWT works. Use that for the threat model. Use the decoder when the token is already in the clipboard.

I will not paste a production token into a site I do not operate, because the token fits in a POST body and a server decoder has a log to keep it in. Doing the work in the tab removes that party. It does not make the token safe on a shared computer or in a screenshot.

No application backend means no application log

The site is a static export. next.config sets output: "export". There is no route handler waiting for a document. Converters, cron presets, status-code pages, and regex presets are HTML and scripts built ahead of time. A paste is an input in the tab. The functions that read it are the same ones the tests import.

"Nothing is uploaded" is the sentence that gets a tool in trouble when it is broader than the code. The narrower sentence is the one I will keep.

There is no application backend, so there is no application log of the paste. There is no account, no history table, and no file store for the text. Image tools that take a file read it through the file input in the tab. They do not post it.

The host still sees the GET, as it would for any static file, and that request does not include the paste. Analytics is a smaller fact. After you use a tool, the page can send a tool_use event whose parameters are the tool name, the action, and the locale. The document you pasted is not one of those parameters. If you block the analytics script, the tools still run. They do not ask a server for permission to parse.

That is a good bargain for a token, a customer row, or a curl command that still carries an Authorization header. It is not a promise about your browser, your extensions, or an employer proxy. A static page cannot see those.

What the type inference does with one JSON sample

The JSON to TypeScript page does not read a schema you already wrote. It looks at one value. The cases that matter are the ones a tidy sample hides: a key missing on some rows, a key that is null on one row and a string on the next, an array with no elements, and an array of objects that do not share a shape.

infer walks the value. null, booleans, and strings stay themselves. A number is an int only when Number.isInteger says so. An empty array has nothing to inspect, so the element type is the internal "any" and the TypeScript printer writes unknown. Inventing string[] from a pair of brackets would be a lie. The guide to the interfaces walks a sample further. The converter is where you paste.

Objects in one array are merged across the union of their keys. A key missing on either object becomes optional. A key on both keeps the merged type, and optional is the OR of the two flags. If one side is null, the other type wins and a later pass marks the field nullable. Two different non-null types become a union. Int and float collapse to float.

If one item omits qty and another sets meta to null, qty is optional and meta is nullable. An empty tags array does not name an element type. A missing key is not promoted to required to tidy the interface.

Kotlin, Swift, PHP, and Dart print that same walk. They do not guess again. PHP uses a constructor with promoted properties and int|null for a nullable field. An empty array is list<mixed> in the docblock. Dart uses a const constructor and factory fromJson. A keyword used as a field name grows a suffix so the class still parses. You get a draft that matches the sample, holes included.

JSON to JSON Schema is the same walk. The document is draft 2020-12. The root object is inline. Nested objects go in $defs and the parent uses $ref. additionalProperties is false. required is the keys present on every merged object. An empty array is { "type": "array" } with no items. A null mixed with a string includes null in the type. One sample is the contract you can see.

Quotes and backslashes in a copied curl command

The curl to Python page starts as a shell tokenizer, not as a search for a URL. A command copied from a browser is wrapped to fit a terminal. The continuation is a backslash and a newline, sometimes a CRLF. tokenize deletes those joins first, including the caret-newline form a Windows copy uses, and then walks the characters.

A single-quoted span is literal until the next quote, and an unterminated quote throws. A double-quoted span honors a backslash before a quote, a backslash, a dollar sign, or a backtick. The $ '...' form turns \n, \t, and \r into those characters. A backslash outside quotes keeps the next character. Whitespace splits words only outside quotes, which is what separates a header with spaces from a URL with &.

parseCurl reads the words. The command has to start with curl. -H takes the next word. -d and the other data flags take a body. --json sets a JSON body and implies the content type. -u is the user, and the Python printer turns it into auth instead of inventing a second header. -k becomes verify=False. A command that disabled certificate checks must not silently grow a check back.

The printer emits requests. Known methods become requests.post or requests.get. Anything else becomes requests.request with the method as a string. A JSON body is reprinted as a Python literal and passed with json=. A raw body stays a string passed with data=. Headers become a dict. With --json, a redundant Content-Type is dropped because requests sets one for json=.

The same parse feeds curl to Axios, curl to Go, and curl to PHP. Axios gets a method, a URL, and headers. Go gets http.NewRequest and Header.Set. PHP gets a curl handle. Multipart in Go stays a comment, because a wrong mime/multipart body is worse than a gap. A command that is not curl fails in words.

Variables, process substitution, and backticks inside the URL are out of scope. If a word looks wrong, look at the quotes and edit the command.

Five cron fields, and where the sentence stops

Cron to English is cronstrue in verbose mode on a 24-hour clock. The sentence updates as you type. The cron presets are the lines I was tired of re-deriving: every five minutes, midnight, weekdays at 09:00, the first of the month. Each preset explains the five fields and then asks the browser for the next five UTC runs through cron-parser. Those times are not written into the HTML at build time. A page built in the morning would otherwise name a midnight that has already passed. Edit the box and the sentence, the list, the crontab line, the GitHub Actions schedule, and the Kubernetes CronJob manifest all follow the same text.

Classic cron, GitHub Actions, and the schedule field of a batch/v1 CronJob are five fields: minute, hour, day of month, month, day of week. There is no seconds field. Quartz and some application frameworks prepend seconds and accept L, W, and #. Those tokens are real there and a quiet disagreement everywhere else. The presets do not use them. A sixth field pasted into a five-field system shifts every other field. The page would rather skip the clever syntax than emit a sentence the cluster will not run.

Sunday is 0, and on many daemons 7 is Sunday too. Monday is 1. A range is inclusive. A list does not fill the gaps. If both day-of-month and day-of-week are restricted, Vixie-style cron fires when either matches. The presets restrict at most one of them. L for the last day is a case these pages will not spell.

GitHub Actions schedules are UTC, will not run more often than every five minutes, can start late, and can be disabled after sixty days without repository activity. A star in the minute field is a legal crontab and a poor workflow. Kubernetes has had spec.timeZone since 1.27. The snippet sets Etc/UTC. A server crontab uses the host zone unless you set one, and the English sentence will not tell you which. The cron examples guide is the longer tour. The preset is the line you copy.

Ten locales, one English canonical, and a duplicate root

The site is built once and served as files. There are ten locales: English, Simplified Chinese, Traditional Chinese, Japanese, Korean, Spanish, French, German, Portuguese, and Russian. A tool page can exist in all of them, because the controls come from dictionaries and the long text is a markdown file per locale. A short translation is not a second article. The indexer strips fenced code and asks for at least three hundred words. A non-English file also has to reach sixty percent of the English body. Below that, the URL may still open, the robots directive is noindex, the locale drops out of hreflang, and the sitemap omits it. English remains the canonical URL for that guide.

The new pages are not translated. generateStaticParams returns English and the slug, and dynamicParams is false, so another locale is a not-found. The language switcher treats converters, cron presets, HTTP status codes, regex patterns, and this essay as English-only. Choosing Japanese there sends you to the Japanese home, where the translated tools still run. A German URL with the same essay under a translated title would compete with the page that should be indexed.

This file lives only in content/en. Hreflang, x-default, and the canonical URL are the English URL.

/ is a permanent redirect to /en, not a second home page. Canonical and x-default stay on /en, and the sitemap does not list / as its own document. Search Console had been treating / and /en as duplicates. The cron hub follows the same shape: one URL, one language.

What you do not get

There is no login. There is nothing to attach a history to, and I do not want a history of pasted values. Refresh the page and the box returns to the sample. That is a loss if you wanted a notebook. It is the point if the last value was a token.

There is no upload in the sense of a file leaving the machine. A converter that accepts an image reads the file the input element provides and encodes it locally. A converter that accepts text uses the textarea. Neither has an endpoint to post to.

There is no share link, because a useful one would have to store the document. Copy writes to the clipboard.

Type inference will not round-trip a schema you already maintain. The curl parser will not send the request. The cron sentence will not register a job. Regex pages say which addresses and which compressed IPv6 forms they skip. The HTTP status pages keep registered codes, including WebDAV, and leave out vendor codes that differ by proxy.

The converters, and the format that is missing

Each link below is the page that does the job. This essay is not trying to stand in for the name of the job.

Data, from a value you already have: JSON to YAML, YAML to JSON, JSON to CSV, CSV to JSON, JSON to XML, XML to JSON, JSON to TOML, TOML to JSON, XML to CSV, JSON to SQL, CSV to SQL, SQL to JSON, CSV to a Markdown table, HTML to Markdown, Markdown to HTML.

Types from one sample: JSON to TypeScript, JSON to Go, JSON to a Python dataclass, JSON to Java, JSON to C#, JSON to Rust, JSON to Zod, JSON to Kotlin, JSON to Swift, JSON to PHP, JSON to Dart, JSON to JSON Schema.

Commands: curl to fetch, curl to Python, curl to Axios, curl to Go, curl to PHP.

Time, color, CSS, encoding, and numbers: cron to English, Unix timestamp to a date, date to a Unix timestamp, epoch milliseconds to a date, hex to RGB, RGB to hex, hex to HSL, px to rem, Base64 to an image, image to Base64, text to Base64, Base64 to text, text to URL encoding, URL encoding to text, binary to decimal, decimal to hex.

The same rule, one case per page, covers cron presets, HTTP status codes, and regex patterns. A pattern page includes a tester already filled in. A status page includes an example response and the codes it is confused with. A preset includes the next runs.

If a format is missing, say which one and what the input looks like. The contact page is enough. A sample you can show in public is more useful than the name of another product. I will add a converter when the parser can be specific. I will not add a page that only repeats this note under a new heading.

Related guides