first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
# @suaveplan/json - Safe JSON parse/stringify with prototype-pollution protection, circular-ref detection, and OTel instrumentation
## Summary
Safe, enterprise-grade JSON utilities with prototype-pollution protection,
circular-reference detection, and OpenTelemetry instrumentation.
<!-- begin:auto-docs -->
## Documentation tree
- [errors](./src/errors/json-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [json-error](./src/errors/json-error.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [safe-json](./src/safe-json/safe-json.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [safe-json](./src/safe-json/safe-json.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [json-error](./src/errors/json-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [safe-json](./src/safe-json/safe-json.md) — <!-- begin:auto-docs --> _(no submodules)_- [safe-json](./src/safe-json/safe-json.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [json-error](./src/errors/json-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [safe-json](./src/safe-json/safe-json.md) — <!-- begin:auto-docs --> _(no submodules)_- [safe-json](./src/safe-json/safe-json.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [json-error](./src/errors/json-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [safe-json](./src/safe-json/safe-json.md) — <!-- begin:auto-docs --> _(no submodules)_- [safe-json](./src/safe-json/safe-json.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [json-error](./src/errors/json-error.md) — <!-- begin:auto-docs --> _(no submodules)_- [safe-json](./src/safe-json/safe-json.md) — <!-- begin:auto-docs --> _(no submodules)_ - [safe-json](./src/safe-json/safe-json.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/json
```
## Quick start
```ts
import { safeParse, safeStringify, tryParse, isValidJSON, getStringifySize } from "@suaveplan/json";
// Safe parse — throws JsonError on invalid JSON or forbidden keys
const obj = safeParse<{ a: number }>('{"a": 1}');
// Safe stringify — throws JsonError on circular references
const json = safeStringify(obj);
// Non-throwing parse
const val = tryParse("bad input"); // undefined
// Validity check (handles "null" correctly)
isValidJSON("null"); // true
isValidJSON("bad"); // false
// UTF-8 byte size of JSON representation
getStringifySize({ hello: "world" }); // 15
```
## Modules
| Module | Description |
|--------|-------------|
| `@suaveplan/json` | Main entry — all public exports |
| `@suaveplan/json/safe-json` | `safeParse`, `safeStringify`, `tryParse`, `isValidJSON`, `getStringifySize` |
| `@suaveplan/json/errors` | `JsonError`, `JSON_CODES`, `JsonCode` |
## Error handling
Every failure is a structured `JsonError` with a stable code:
| Code | Trigger |
|------|---------|
| `JSON.PARSE.FAILED` | Invalid JSON syntax or aborted `AbortSignal` |
| `JSON.PARSE.PROTOTYPE_POLLUTION` | Forbidden key (`__proto__`, `constructor`, `prototype`) |
| `JSON.STRINGIFY.CIRCULAR` | True circular reference detected |
| `JSON.STRINGIFY.FAILED` | Aborted signal or non-serialisable value (e.g. `BigInt`) |
```ts
import { safeParse, JsonError, JSON_CODES } from "@suaveplan/json";
try {
safeParse('{"__proto__": {}}');
} catch (err) {
if (err instanceof JsonError && err.code === JSON_CODES["PARSE.PROTOTYPE_POLLUTION"]) {
console.error("Prototype pollution blocked");
}
}
```
## Options
### `ParseOptions`
```ts
interface ParseOptions {
forbiddenKeys?: string[]; // default: ["__proto__", "constructor", "prototype"]
maxDepth?: number; // default: 50
signal?: AbortSignal;
}
```
### `StringifyOptions`
```ts
interface StringifyOptions {
space?: number;
signal?: AbortSignal;
maxDepth?: number; // default: 100
}
```
## Benchmarks
Measured on Intel Xeon E5-2696 v4 @ 2.20 GHz, Bun 1.3.13:
```text
benchmark avg (min … max)
-------------------------------------------
safeParse small 9.84 µs/iter
JSON.parse small 592.70 ns/iter
safeStringify small 1.99 µs/iter
JSON.stringify small 212.99 ns/iter
safeParse medium 42.11 µs/iter
safeStringify medium 11.92 µs/iter
```
The overhead relative to the native functions comes from the OTel span
creation, reviver/replacer function calls, and ancestor-stack tracking.
In production with a no-op tracer the spans cost ~0 ns.
## Links
- [`src/safe-json/safe-json.md`](src/safe-json/safe-json.md) — implementation notes
- [`src/errors/json-error.md`](src/errors/json-error.md) — error codes
- [`src/types/types.md`](src/types/types.md) — option types

View File

@@ -0,0 +1,103 @@
# json-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides the typed error class and stable code registry for `@suaveplan/json`.
All error conditions produced by this package surface as `JsonError` instances
with machine-readable codes so callers can branch on failure mode without
parsing message strings.
## Features
- `JSON_CODES` registry — four stable codes under the `JSON.*` namespace
- `JsonCode` union type for compile-time exhaustiveness checks
- `JsonError` extends `SuaveplanError` — carries `code`, `cause`, and optional
context payload; integrates with the OTel span-event adapter from
`@suaveplan/error`
## Usage: Basic Example
```ts
import { JsonError, JSON_CODES } from "@suaveplan/json/errors";
throw new JsonError(
JSON_CODES["PARSE.FAILED"],
"Unexpected token at position 0"
);
```
## Usage: Advanced Example
```ts
import { JsonError, JSON_CODES } from "@suaveplan/json/errors";
function parseSafe(text: string): unknown {
try {
return JSON.parse(text);
} catch (cause) {
throw new JsonError(
JSON_CODES["PARSE.FAILED"],
`JSON parse failed: ${(cause as Error).message}`,
{ cause }
);
}
}
```
## API Reference
### `JSON_CODES`
```ts
const JSON_CODES: {
"PARSE.FAILED": "JSON/PARSE.FAILED";
"PARSE.PROTOTYPE_POLLUTION": "JSON/PARSE.PROTOTYPE_POLLUTION";
"STRINGIFY.CIRCULAR": "JSON/STRINGIFY.CIRCULAR";
"STRINGIFY.FAILED": "JSON/STRINGIFY.FAILED";
};
```
Stable, namespaced error codes produced by `defineCodes("JSON", [...])`.
Each code is a string literal of the form `"JSON/<segment>"`.
### `JsonCode`
```ts
type JsonCode = (typeof JSON_CODES)[keyof typeof JSON_CODES];
```
Union of all four code literals. Use this type for exhaustive `switch`
statements or discriminated-union handlers.
### `class JsonError`
```ts
class JsonError extends SuaveplanError<JsonCode> {
constructor(code: JsonCode, message: string, options?: SuaveplanErrorOptions);
}
```
**Parameters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `code` | `JsonCode` | Machine-readable error code from `JSON_CODES`. |
| `message` | `string` | Human-readable description of the error. |
| `options` | `SuaveplanErrorOptions?` | Optional `cause`, `context`, and `meta` fields. |
**Throws** — never; constructor only constructs.
## Implementation Notes
The constructor is flagged with `// biome-ignore lint/complexity/noUselessConstructor`
because it widens the visibility of the `SuaveplanError` constructor from
`protected` to `public`. This is required by the Biome linter which otherwise
flags it as a no-op, but the widening is intentional and load-bearing.
Error codes are registered once at module load time via `defineCodes`. The
`defineCodes` helper from `@suaveplan/error` is idempotent within a process
(same namespace + same codes → same object reference), making it safe to import
across multiple entry points without duplication.

View File

@@ -0,0 +1,209 @@
# safe-json
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides enterprise-grade `JSON.parse` and `JSON.stringify` wrappers that
defend against prototype pollution, circular references, and AbortSignal
cancellation. Every error surface is a structured `JsonError` with a stable,
machine-readable code — no raw `SyntaxError` instances escape this module.
All I/O-critical entry points emit OpenTelemetry spans for distributed tracing.
## Features
- `safeParse` — parses JSON with a reviver that blocks forbidden keys
(`__proto__`, `constructor`, `prototype` by default); configurable via
`ParseOptions.forbiddenKeys`
- `safeStringify` — stringifies with ancestor-stack cycle detection; DAG
sharing (same object at two different paths) is allowed; only true cycles
throw `STRINGIFY.CIRCULAR`
- `tryParse` — non-throwing wrapper returning `undefined` on any error
- `isValidJSON` — boolean check that correctly handles the `"null"` literal
- `getStringifySize` — UTF-8 byte size of the JSON representation
- OTel spans on `safeParse` and `safeStringify`
- AbortSignal support on both `safeParse` and `safeStringify`
## Usage: Basic Example
```ts
import { safeParse, safeStringify, tryParse, isValidJSON, getStringifySize } from "@suaveplan/json";
// Parse
const obj = safeParse<{ a: number }>('{"a":1}');
console.log(obj.a); // 1
// Stringify
const json = safeStringify({ a: 1 });
console.log(json); // '{"a":1}'
// Non-throwing parse
const result = tryParse("not-json"); // undefined
// Validity check
isValidJSON("null"); // true
isValidJSON("bad"); // false
// Byte size
getStringifySize({ hello: "world" }); // 15
```
## Usage: Advanced Example
```ts
import {
safeParse,
safeStringify,
JsonError,
JSON_CODES,
type ParseOptions,
type StringifyOptions,
} from "@suaveplan/json";
// Block additional keys beyond the defaults
const parseOpts: ParseOptions = {
forbiddenKeys: ["__proto__", "constructor", "prototype", "eval"],
};
try {
const data = safeParse<unknown>('{"eval":"alert(1)"}', parseOpts);
} catch (err) {
if (err instanceof JsonError && err.code === JSON_CODES["PARSE.PROTOTYPE_POLLUTION"]) {
console.error("Prototype pollution attempt blocked");
}
}
// AbortSignal cancellation
const controller = new AbortController();
setTimeout(() => controller.abort(), 10);
const stringifyOpts: StringifyOptions = {
space: 2,
signal: controller.signal,
};
const largePayload = Array.from({ length: 10000 }, (_, i) => ({ id: i }));
try {
safeStringify(largePayload, stringifyOpts);
} catch (err) {
if (err instanceof JsonError && err.code === JSON_CODES["STRINGIFY.FAILED"]) {
console.error("Stringify cancelled");
}
}
// Circular reference detection
const node: Record<string, unknown> = { name: "root" };
node.self = node;
try {
safeStringify(node);
} catch (err) {
if (err instanceof JsonError && err.code === JSON_CODES["STRINGIFY.CIRCULAR"]) {
console.error("Circular reference blocked");
}
}
```
## API Reference
### `safeParse<T>(text, options?): T`
Parses a JSON string with prototype-pollution protection.
**Parameters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `text` | `string` | Raw JSON text to parse. |
| `options` | `ParseOptions?` | Optional parse configuration. |
**Returns** — The parsed value typed as `T`.
**Throws**
- `JsonError(PARSE.FAILED)` — invalid JSON syntax or aborted signal.
- `JsonError(PARSE.PROTOTYPE_POLLUTION)` — a forbidden key was encountered.
---
### `safeStringify(value, options?): string`
Stringifies a value with circular-reference detection.
**Parameters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `value` | `unknown` | Value to serialise. |
| `options` | `StringifyOptions?` | Optional stringify configuration. |
**Returns** — The JSON string.
**Throws**
- `JsonError(STRINGIFY.CIRCULAR)` — a true cycle was detected.
- `JsonError(STRINGIFY.FAILED)` — aborted signal or non-serialisable value (e.g. `BigInt`).
---
### `tryParse<T>(text, options?): T | undefined`
Non-throwing parse; returns `undefined` on any error.
**Parameters** — same as `safeParse`.
**Returns** — The parsed value or `undefined`.
---
### `isValidJSON(text): boolean`
Returns `true` if the string is parseable by `safeParse` (respects the default
forbidden-key list). Returns `true` for `"null"` (unlike a naïve
`tryParse(text) !== undefined` check).
**Parameters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `text` | `string` | The string to test. |
**Returns** — `boolean`.
---
### `getStringifySize(value): number`
**Parameters**
| Parameter | Type | Description |
|-----------|------|-------------|
| `value` | `unknown` | Any JSON-serialisable value. |
**Returns** — UTF-8 byte count of `JSON.stringify(value)`.
## Implementation Notes
**Prototype pollution guard** — `JSON.parse` supports a reviver function that
is called for every key-value pair during object construction. The reviver
inspects each key against a `Set<string>` built from `forbiddenKeys` (default:
`__proto__`, `constructor`, `prototype`). The root `""` key is exempt because
the reviver is called with the empty string for the top-level result.
**Cycle detection** — The replacer receives `this` bound to the parent object at
each step, which allows reconstructing the ancestor stack. When entering a
child object we find the parent's index in the stack and truncate everything
after it, then check whether the new value is already in the stack before
pushing it. This correctly allows DAG sharing (the same object referenced
at two sibling paths) while rejecting true back-edges.
**`isValidJSON` and the `"null"` edge case** — A naïve implementation using
`tryParse(text) !== undefined` would return `false` for the `"null"` literal
because `JSON.parse("null")` returns `null`. This module instead wraps
`safeParse` in a `try/catch` and returns `true` on success.
**OTel instrumentation** — `safeParse` and `safeStringify` each create an
active span via `trace.getTracer("@suaveplan/json").startActiveSpan(...)`.
When no SDK is configured the tracer is a no-op, so there is zero overhead
in un-instrumented environments.

View File

@@ -0,0 +1,78 @@
# types
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the public option types consumed by `@suaveplan/json` operations.
Centralising these interfaces in a dedicated `types/` module keeps the
implementation files focused on logic and lets external callers import only the
types they need without pulling in any runtime code.
## Features
- `ParseOptions` — controls forbidden-key list, max nesting depth, and abort
signal for `safeParse` / `tryParse`
- `StringifyOptions` — controls pretty-print spacing, abort signal, and
circular-ref depth limit for `safeStringify`
## Usage: Basic Example
```ts
import type { ParseOptions } from "@suaveplan/json";
const opts: ParseOptions = {
forbiddenKeys: ["__proto__", "constructor"],
maxDepth: 20,
};
```
## Usage: Advanced Example
```ts
import type { ParseOptions, StringifyOptions } from "@suaveplan/json";
const controller = new AbortController();
const parseOpts: ParseOptions = {
forbiddenKeys: ["__proto__", "constructor", "prototype", "eval"],
maxDepth: 10,
signal: controller.signal,
};
const stringifyOpts: StringifyOptions = {
space: 2,
signal: controller.signal,
maxDepth: 50,
};
// Abort after 500 ms
setTimeout(() => controller.abort(), 500);
```
## API Reference
### `ParseOptions`
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `forbiddenKeys` | `string[]?` | `["__proto__", "constructor", "prototype"]` | Keys that trigger a `PARSE.PROTOTYPE_POLLUTION` error when encountered anywhere in the object tree. |
| `maxDepth` | `number?` | `50` | Maximum JSON nesting depth; deeper structures throw `PARSE.FAILED`. |
| `signal` | `AbortSignal?` | — | Optional cancellation token; aborted signals cause `safeParse` to throw `PARSE.FAILED`. |
### `StringifyOptions`
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `space` | `number?` | — | Forwarded verbatim to `JSON.stringify`; produces indented output when set. |
| `signal` | `AbortSignal?` | — | Optional cancellation token; aborted signals cause `safeStringify` to throw `STRINGIFY.FAILED`. |
| `maxDepth` | `number?` | `100` | Maximum depth before `safeStringify` throws `STRINGIFY.CIRCULAR`. |
## Implementation Notes
Both interfaces are pure TypeScript — no runtime object is emitted. The types
file is excluded from coverage measurement because there is nothing to execute.
Default values are applied inside the respective implementation functions
(`safeParse`, `safeStringify`) using object destructuring with defaults, keeping
the type definitions free of runtime assumptions.