karsh-json

Dil
TypeScript
Yıldız
0
Fork
0
Son güncelleme
Lisans
MIT

Etiketler: jsonn8ntypescript

GitHub'da aç ↗

README

karsh-json

Hatanın nerede ve neden olduğunu söyleyen bir JSON ayrıştırıcı; yanına da yapıştırılan "neredeyse JSON"u onaran, ağacı geri yazan, sır arayan ve n8n workflow dosyalarını okuyan parçalar. Bağımlılığı yok.

A JSON parser that says what is wrong, where and why — with repair for the near-JSON people actually paste, a printer that changes nothing but whitespace, a secret scanner and an n8n workflow reader beside it. No dependencies.


Türkçe

Ne işe yarar

JSON.parse neyin yanlış olduğunu aslında bilir ama bir ayrıştırıcının diliyle söyler: fazladan virgül "Expected double-quoted property name" olur ve ondan sonraki paranteze işaret eder; tek tırnakla yazılmış bir anahtarla tırnaksız bir anahtar aynı mesajı alır. Bu paket tam olarak JSON.parse'ın kabul ettiği dili kabul eder (ona karşı fuzz'landı) ve durduğu yeri insanın söyleyeceği gibi söyler: satır, sütun, sorunlu metin ve hâlâ açık duran parantez.

Bir de JSON.parse'ın attığı şeyleri saklar: sayılar ve dizeler yazıldıkları metin olarak durur, üyeler belge sırasını ve yinelenenleri korur. Bu yüzden biçimlendirme 12345678901234567890'ı 12345678901234567000 yapmaz, 1.0'ı 1'e indirmez, aynı anahtarın ikincisini silmez.

Yedi dosya, yedi iş:

Dosya İş
parse Konum bildiren ayrıştırıcı; ağaç, istatistik ve yinelenen anahtarlar.
repair JavaScript nesnesi, JSON5, Python çıktısı ya da sohbetten gelen metni JSON'a çevirir.
print Ağacı geri yazar: girintili ya da tek satır, istenirse anahtarlar sıralı.
secrets Bir dizenin içindeki anahtar/parola: hem bilinen biçimler hem "şüpheli isim + düz değer".
n8n Workflow dışa aktarımını okur: düğümler, tetikleyiciler, bağlantılar, sızıntılar, temizlenmiş kopya.
decode Açılan dosyanın baytlarını metne çevirir (UTF-8 ve iki UTF-16).
check Hepsi tek çağrıda — arayüzün ihtiyaç duyduğu tek geçiş.

Kurulum

Paket derlenmiş dosya taşımaz; TypeScript kaynağı olduğu gibi yayımlanır (exports doğrudan src/index.ts'i gösterir). Build adımı yoktur.

npm i github:HasanKarsi/karsh-json

Node 18 ve üzeri (atob ve structuredClone için), ya da herhangi bir tarayıcı.

Kullanım

1. Hata nerede?

import { parseJson, errorExcerpt } from "karsh-json";

const text = `{
  "ad": "KARSH",
  "sayi": 12,
}`;

const result = parseJson(text);
// { ok: false, error: { code: "trailingComma", offset: 31, line: 3, column: 13, expected: "}" } }

if (!result.ok) {
  errorExcerpt(text, result.error.offset);
  // { before: '  "sayi": 12', after: ",", clippedStart: false, clippedEnd: false }
  // `before` imlece kadar olan kısım: ok satırını yazı tipine bırakmadan çizebilirsin.
}

offset, setSelectionRange'in beklediği UTF-16 konumudur; column ise kod noktasıyla sayılır, yani satırın başındaki bir emoji bir kolon sayılır.

2. Yapıştırılanı onar, sonra olduğu gibi geri yaz

import { repairJson, parseJson, printJson } from "karsh-json";

repairJson("{ad: 'KARSH', /* not */ liste: [1,2,],}");
// { text: '{"ad": "KARSH",   "liste": [1,2]}', changed: true,
//   fixes: { trailingComma: 2, comment: 1, quote: 1, unquotedKey: 2, literal: 0, invisible: 0, controlChar: 0 } }

const parsed = parseJson('{"b":1,"a":12345678901234567890,"n":1.0,"b":2}');
if (parsed.ok) {
  parsed.duplicates;
  // [{ key: "b", path: [], offset: 40, line: 1, column: 41 }] — ikisi de duruyor

  printJson(parsed.root, { indent: "  ", sortKeys: true });
  // {
  //   "a": 12345678901234567890,   <- basamaklar aynı
  //   "b": 1,
  //   "b": 2,                      <- yinelenen silinmedi
  //   "n": 1.0                     <- 1.0, "1" değil
  // }
}

Geçerli JSON repairJson'dan bayt bayt aynı çıkar (fuzz'landı); düğmeyi göstermeyi güvenli kılan da bu. Kapanış tırnağı temiz bulunamayan bir dize olduğu gibi bırakılır — tahmin edilmez, ayrıştırıcı ona işaret edebilsin diye.

3. Sır ara, workflow'u oku

import { scanString, analyseN8n, sanitise } from "karsh-json";

scanString("Bearer sk-ant-api03-...", null); // "..." anahtarın kalanı, tam metin 115 karakter
// [{ kind: "anthropic", start: 7, end: 115, masked: "sk-a••••••••" }] — biçim tanınıyorsa ad gerekmez

scanString("hunter2", "password");          // [{ kind: "named", … }] — şüpheli ad + düz değer
scanString("={{ $credentials.token }}", "password"); // [] — bu bir referans, sır değil

const report = analyseN8n(workflow);
report?.workflows[0].triggers;
// [{ node: "Webhook", kind: "webhook", type: "n8n-nodes-base.webhook", detail: "POST /orders" }]
report?.workflows[0].findings;
// [{ node: "HTTP Request", path: "parameters.headerParameters.parameters[0].value",
//    field: "Authorization", kind: "anthropic", masked: "sk-a••••••••" }]
report?.workflows[0].webhooks;
// [{ node: "Webhook", method: "POST", path: "orders", open: true }] — kimlik doğrulaması yok

sanitise(workflow, report, 2);
// "Bearer <redacted:anthropic-api-key>" — paylaşılabilir kopya: sırlar, sabitlenmiş
// çalışma verisi ve instance id çıkarılmış hâli

Ya da hepsi tek çağrıda:

import { check } from "karsh-json";

check(text);
// { text, result, bytes: 34, lines: 4,
//   n8n: null,                                  <- n8n dışa aktarımı değil
//   repair: { result: …, valid: true } }        <- onarım gerçekten çözüyor, düğme sözünü tutar

API

İmza Ne döner
check(text) Tek geçiş: ayrıştırma, bayt ve satır sayısı, n8n raporu (varsa) ve onarım önizlemesi.
parseJson(text) { ok: true, root, stats, duplicates, duplicateCount } ya da { ok: false, error }. error: kod, offset, line, column, found, expected ve açık kalan parantez.
locate(text, offset) · errorExcerpt(text, offset, width) Konumdan satır/sütun; ve hatanın çevresi — iki megabaytlık tek satırlık dosyada bile yalnızca çevresi okunur.
formatPath(path) · utf8Length(text) · countLines(text) nodes[0].parameters["odd key"] biçiminde yol; gerçek bayt uzunluğu; satır sayısı.
repairJson(input) { text, changed, fixes } — hangi düzeltmeden kaç tane yapıldığı sayılır.
printJson(root, { indent, sortKeys }) Ağacı geri yazar. indent: iki boşluk, dört boşluk, sekme ya da null (tek satır).
PRINT_LIMIT · OutputTooLargeError Çıktı tavanı: on bin iç içe dizi gigabaytlarca metne açılır, bu onu ölü sekme yerine açıklanabilir bir hataya çevirir.
scanString(value, name) Bir dizedeki bulgular: bilinen biçimler her yerde; "şüpheli ad + düz değer" ise yalnızca gerçekten değer yazılmışsa.
redact(value, hits) · mask(value) · placeholder(kind) Değeri yer tutucuyla değiştir; ekranda göstermek için maskele; bir tür için yer tutucu metni.
nameStrength(name) · isSecretName(name) · isPlaceholder(value) Ad ne kadar bağlayıcı (password, secret, weak); bu ad bir sır adı mı; bu değer zaten yer tutucu mu.
mightBeN8n(root) · isWorkflow(value) Ucuz ön eleme (ağaç üzerinde) ve kesin kontrol (nesneler üzerinde).
analyseN8n(value) Düğümler, tetikleyiciler, bağlantılar, kopuk uçlar, yalnız düğümler, kimlik referansları, sızıntılar, sabitlenmiş veri, webhook'lar. n8n değilse null.
sanitise(value, report, indent) Paylaşılabilir kopya: sırlar yer tutucuyla, sabitlenmiş çalışma verisi ve instance id çıkarılmış.
decodeText(bytes) Baytları metne çevirir (UTF-8, UTF-16LE, UTF-16BE); ikili dosyada null.
isEditOf(previous, next) · errorSlots(error) Yeni metin öncekinin düzenlenmiş hâli mi (imleci korumak için); ve hata mesajındaki boşlukların dolduracağı değerler.

Neden kütüphane değil, elle yazıldı

Çünkü istenen şey "ayrıştır" değil, "neyin yanlış olduğunu göster"di. JSON.parse bir istisna atar ve mesajı motordan motora değişir; satır ve sütun verse bile fazladan virgülü virgülün kendisinde değil ondan sonra gelen karakterde gösterir. Bir editörün imleci oraya götürebilmesi için hatanın yapılandırılmış olması gerekiyordu: kod, konum, sorunlu metin, açık parantez.

İkinci sebep sadakat. Bir biçimlendirici JSON.parse + JSON.stringify üstüne kurulursa, büyük tam sayıları sessizce bozar, 1.0'ı 1 yapar, anahtarları JavaScript'in nesne sırasına sokar (tam sayı gibi duran anahtarlar başa geçer) ve yinelenen anahtarın ilkini atar. Bu paket ölçekleri metin olarak sakladığı için biçimlendirme yalnızca boşluğu değiştirir — yinelenen anahtarı da uyarı olarak gösterebilir, çünkü hâlâ oradadır.

Üçüncüsü sınır davranışı: V8'in JSON.parse'ı çağrı yığınının kaldırdığından çok daha derin iç içe geçmeyi kabul eder. "Tam olarak JSON.parse'ın kabul ettiğini kabul eder" sözü orada da tutulsun diye ayrıştırıcı özyinelemesiz yazıldı: açık bir yığınla, döngüyle.

n8n tarafı da aynı sebeple burada: bir workflow dışa aktarımında şifreler şifreli veritabanında kalır, ama insanların parametrelere yazdıkları kalmaz — başlık alanına yapıştırılan anahtar, Code düğümündeki token, bağlantı dizesindeki parola. Bunu arayan hazır bir şey yoktu; olsa bile {{ }} içindeki bir referansla düz bir parolayı ayırt etmesi gerekirdi.

Lisans

MIT — bkz. LICENSE.


English

What it is for

JSON.parse knows exactly what is wrong, but it says it in a parser's terms: a trailing comma becomes "Expected double-quoted property name", pointed at the bracket after it, and a single-quoted key and an unquoted key get the same message. This package accepts precisely the language JSON.parse accepts (fuzzed against it) and names where it stopped the way a person would: the line and column, the offending text, and which bracket is still open.

It also keeps what JSON.parse throws away: scalars stay as the text they were written as, and members keep document order and duplicates. So formatting never turns 12345678901234567890 into 12345678901234567000, or 1.0 into 1, and never drops the first of two identical keys.

Seven files, seven jobs:

File Job
parse The locating parser: tree, statistics, duplicate keys.
repair Turns a JavaScript object literal, JSON5, a Python printout or a chat message into JSON.
print Writes the tree back out: indented or on one line, keys sorted on request.
secrets A key or password inside a string: known formats, and "suspicious name plus a literal value".
n8n Reads a workflow export: nodes, triggers, connections, leaks, sanitised copy.
decode The bytes of an opened file as text (UTF-8 and both UTF-16s).
check All of it in one call — the single pass an interface needs.

Install

No build step and no compiled files: the package ships TypeScript source and exports points straight at src/index.ts.

npm i github:HasanKarsi/karsh-json

Node 18 or newer (for atob and structuredClone), or any browser.

Usage

1. Where is the error?

import { parseJson, errorExcerpt } from "karsh-json";

const text = `{
  "ad": "KARSH",
  "sayi": 12,
}`;

const result = parseJson(text);
// { ok: false, error: { code: "trailingComma", offset: 31, line: 3, column: 13, expected: "}" } }

if (!result.ok) {
  errorExcerpt(text, result.error.offset);
  // { before: '  "sayi": 12', after: ",", clippedStart: false, clippedEnd: false }
  // `before` ends just before the offending character, so the caret row can be drawn from it.
}

offset is the UTF-16 offset setSelectionRange takes; column counts code points, so an emoji earlier on the line counts once.

2. Repair what was pasted, then write it back unchanged

import { repairJson, parseJson, printJson } from "karsh-json";

repairJson("{ad: 'KARSH', /* note */ liste: [1,2,],}");
// { text: '{"ad": "KARSH",   "liste": [1,2]}', changed: true,
//   fixes: { trailingComma: 2, comment: 1, quote: 1, unquotedKey: 2, literal: 0, invisible: 0, controlChar: 0 } }

const parsed = parseJson('{"b":1,"a":12345678901234567890,"n":1.0,"b":2}');
if (parsed.ok) {
  parsed.duplicates;
  // [{ key: "b", path: [], offset: 40, line: 1, column: 41 }] — both are still there

  printJson(parsed.root, { indent: "  ", sortKeys: true });
  // {
  //   "a": 12345678901234567890,   <- every digit kept
  //   "b": 1,
  //   "b": 2,                      <- the duplicate is not dropped
  //   "n": 1.0                     <- 1.0, not 1
  // }
}

Valid JSON comes out of repairJson byte for byte (fuzzed), which is what makes it safe to offer the button at all. A string whose closing quote cannot be found cleanly is left exactly as it was — nothing is guessed, so the parser can point at it.

3. Look for secrets, read the workflow

import { scanString, analyseN8n, sanitise } from "karsh-json";

scanString("Bearer sk-ant-api03-...", null); // "..." stands for the rest of the key; 115 characters in all
// [{ kind: "anthropic", start: 7, end: 115, masked: "sk-a••••••••" }] — a known format needs no name

scanString("hunter2", "password");                   // [{ kind: "named", … }] — name plus a literal
scanString("={{ $credentials.token }}", "password"); // [] — a reference, not a secret

const report = analyseN8n(workflow);
report?.workflows[0].triggers;
// [{ node: "Webhook", kind: "webhook", type: "n8n-nodes-base.webhook", detail: "POST /orders" }]
report?.workflows[0].findings;
// [{ node: "HTTP Request", path: "parameters.headerParameters.parameters[0].value",
//    field: "Authorization", kind: "anthropic", masked: "sk-a••••••••" }]
report?.workflows[0].webhooks;
// [{ node: "Webhook", method: "POST", path: "orders", open: true }] — takes calls with no auth

sanitise(workflow, report, 2);
// "Bearer <redacted:anthropic-api-key>" — the shareable copy, with secrets, pinned
// execution data and the instance id removed

Or all of it at once:

import { check } from "karsh-json";

check(text);
// { text, result, bytes: 34, lines: 4,
//   n8n: null,                           <- not an n8n export
//   repair: { result: …, valid: true } } <- the repair really does parse, so the button can promise it

API

Signature Returns
check(text) One pass: the parse, bytes and lines, the n8n report when there is one, and a repair preview.
parseJson(text) { ok: true, root, stats, duplicates, duplicateCount } or { ok: false, error }. The error carries a code, offset, line, column, found, expected and the bracket still open.
locate(text, offset) · errorExcerpt(text, offset, width) Line and column from an offset; and the neighbourhood of the error — only the neighbourhood is read, even in a two-megabyte single line.
formatPath(path) · utf8Length(text) · countLines(text) A path as nodes[0].parameters["odd key"]; the real byte length; the line count.
repairJson(input) { text, changed, fixes }, with each kind of fix counted.
printJson(root, { indent, sortKeys }) Writes the tree back out. indent: two spaces, four spaces, a tab, or null for one line.
PRINT_LIMIT · OutputTooLargeError A ceiling on the output: ten thousand nested arrays format to gigabytes, and this turns that into an error an interface can explain instead of a dead tab.
scanString(value, name) Findings in one string: a known format anywhere, a "suspicious name plus a literal" only when a value was really typed.
redact(value, hits) · mask(value) · placeholder(kind) Replace a value with a placeholder; mask it for display; the placeholder text for a kind.
nameStrength(name) · isSecretName(name) · isPlaceholder(value) How binding the name is (password, secret, weak); whether it is a secret's name at all; whether the value is already a placeholder.
mightBeN8n(root) · isWorkflow(value) The cheap look (over the tree) and the real check (over objects).
analyseN8n(value) Nodes, triggers, connections, dangling edges, isolated nodes, credential references, leaks, pinned data, webhooks. null when it is not n8n.
sanitise(value, report, indent) The shareable copy: secrets replaced, pinned execution data and the instance id removed.
decodeText(bytes) Bytes as text (UTF-8, UTF-16LE, UTF-16BE); null for binary data.
isEditOf(previous, next) · errorSlots(error) Whether the new text reads as an edit of the old one (so a cursor can be kept); and the values a message's slots are filled with.

Why this is written out rather than pulled in

Because what was needed was not "parse it" but "show what is wrong". JSON.parse throws, its message differs between engines, and even where it gives a line and column it points at the character after the trailing comma rather than at the comma. For an editor to move the caret there, the error had to be structured: a code, a position, the offending text, the open bracket.

The second reason is fidelity. A formatter built on JSON.parse plus JSON.stringify quietly breaks large integers, turns 1.0 into 1, reorders keys into JavaScript's object order (integer-like keys hoisted to the front) and drops the first of two identical keys. Here scalars are kept as text, so formatting changes whitespace only — and duplicates can be reported as a warning, because they are still there.

The third is the edges: V8's JSON.parse accepts nesting far deeper than the call stack allows. For "accepts exactly what JSON.parse accepts" to hold there too, the parser is iterative — an explicit stack, no recursion.

The n8n part is here for the same reason. In a workflow export the credentials themselves stay encrypted in n8n's database, but what people type into parameters does not: the key pasted into a header field, the token in a Code node, the password in a connection string. Nothing off the shelf looked for that, and anything that did would still have to tell a reference inside {{ }} apart from a password typed in plain.

License

MIT — see LICENSE.

← Tüm depolar