@williamthorsen/readout 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ [
2
+ {
3
+ "version": "0.1.0",
4
+ "date": "2026-11-06",
5
+ "sections": [
6
+ {
7
+ "title": "🎉 Features",
8
+ "audience": "all",
9
+ "items": [
10
+ {
11
+ "description": "Adds the `@williamthorsen/readout` package, whose `Readout` type pairs a result's data with a list of its sensitive top-level fields, and whose `serializeReadout` and `redactReadout` leave those fields out and name them under `redacted` unless the caller passes `includeSensitive: true`. (#8)",
12
+ "hash": "0495317f861e1f9f71b18aa0379a62dbfc77ce4c",
13
+ "entry": 1
14
+ },
15
+ {
16
+ "description": "Adds `embedJson`, which escapes every `<` in JSON text so that the text can be the content of a `<script type=\"application/json\">` element without closing the element or opening a comment. (#8)",
17
+ "hash": "0495317f861e1f9f71b18aa0379a62dbfc77ce4c",
18
+ "entry": 2
19
+ },
20
+ {
21
+ "description": "Adds the `Renderers` type, which maps each type of value to its renderer and fails the typecheck when a type lacks one, and the `render` function, which dispatches a value to the renderer for its `type`. (#8)",
22
+ "hash": "0495317f861e1f9f71b18aa0379a62dbfc77ce4c",
23
+ "entry": 3
24
+ }
25
+ ]
26
+ },
27
+ {
28
+ "title": "⚙️ Tooling",
29
+ "audience": "dev",
30
+ "items": [
31
+ {
32
+ "description": "Adds a `typecheck:post` script that typechecks the published source of `packages/readout` through `tsconfig.src.json`, which excludes the tests, so that the DOM lib loaded by a test's `/// <reference lib=\"dom\" />` cannot hide a DOM API used in `src/`. (#8)",
33
+ "hash": "0495317f861e1f9f71b18aa0379a62dbfc77ce4c",
34
+ "entry": 4
35
+ }
36
+ ]
37
+ }
38
+ ]
39
+ }
40
+ ]
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ ## 0.1.0 — 2026-11-06
6
+
7
+ ### 🎉 Features
8
+
9
+ - Adds the `@williamthorsen/readout` package, whose `Readout` type pairs a result's data with a list of its sensitive top-level fields, and whose `serializeReadout` and `redactReadout` leave those fields out and name them under `redacted` unless the caller passes `includeSensitive: true`. (#8)
10
+ - Adds `embedJson`, which escapes every `<` in JSON text so that the text can be the content of a `<script type="application/json">` element without closing the element or opening a comment. (#8)
11
+ - Adds the `Renderers` type, which maps each type of value to its renderer and fails the typecheck when a type lacks one, and the `render` function, which dispatches a value to the renderer for its `type`. (#8)
12
+
13
+ ### ⚙️ Tooling
14
+
15
+ - Adds a `typecheck:post` script that typechecks the published source of `packages/readout` through `tsconfig.src.json`, which excludes the tests, so that the DOM lib loaded by a test's `/// <reference lib="dom" />` cannot hide a DOM API used in `src/`. (#8)
16
+
17
+ <!-- Generated by release-kit. Do not edit this file. Use .meta/changelog-overrides.json to override entries. -->
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 William Thorsen
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,65 @@
1
- # Temporary Holding Version
1
+ <!-- readme-type: library -->
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # @williamthorsen/readout
4
+
5
+ One result object that a person and a machine both read: a redacting JSON serializer, a DOM-embedding helper, and typed renderers over the same object.
6
+
7
+ <!-- section:release-notes -->
8
+ ## Release notes — v0.1.0 (2026-11-06)
9
+
10
+ ### 🎉 Features
11
+
12
+ - Adds the `@williamthorsen/readout` package, whose `Readout` type pairs a result's data with a list of its sensitive top-level fields, and whose `serializeReadout` and `redactReadout` leave those fields out and name them under `redacted` unless the caller passes `includeSensitive: true`. (#8)
13
+ - Adds `embedJson`, which escapes every `<` in JSON text so that the text can be the content of a `<script type="application/json">` element without closing the element or opening a comment. (#8)
14
+ - Adds the `Renderers` type, which maps each type of value to its renderer and fails the typecheck when a type lacks one, and the `render` function, which dispatches a value to the renderer for its `type`. (#8)
15
+ <!-- /section:release-notes -->
16
+
17
+ ```ts
18
+ import { type Readout, serializeReadout } from '@williamthorsen/readout';
19
+
20
+ const readout: Readout<{ id: string; secret: string }> = { data: { id: 'tok-1', secret: 'ATATT-…' }, sensitive: ['secret'] };
21
+ serializeReadout(readout); // {"id": "tok-1", "redacted": ["secret"]}, indented by two spaces
22
+ ```
23
+
24
+ ## Why
25
+
26
+ A result that an app shows to a person, such as a created token or a check's outcome, also has a reader that drives the app: an agent, a script, or an end-to-end test. When the result exists only as formatted text, that reader parses prose that changes whenever the wording does. `readout` keeps the result as one object in canonical values and renders every form from it, so that the forms cannot disagree. The convention that it implements is the `machine-readable-output-conventions` rulebook in [codeassembly-guidance](https://github.com/williamthorsen/codeassembly-guidance).
27
+
28
+ Hand-rolled `JSON.stringify` covers the machine form, but each call site then decides on its own what to leave out, and a secret reaches a page snapshot through the call site that forgot. A readout declares its sensitive fields once, in its type.
29
+
30
+ The package has no runtime dependency and uses neither DOM nor Node APIs, so it runs in a browser and in Node alike.
31
+
32
+ ## One readout, two forms
33
+
34
+ ```ts
35
+ import { embedJson, type Readout, render, type Renderers, serializeReadout } from '@williamthorsen/readout';
36
+
37
+ interface TokenCreated {
38
+ type: 'created';
39
+ id: string;
40
+ expiresAt: string;
41
+ secret: string;
42
+ }
43
+
44
+ const readout: Readout<TokenCreated> = {
45
+ data: { type: 'created', id: 'tok-1', expiresAt: '2026-11-12T00:00:00Z', secret: 'ATATT-…' },
46
+ sensitive: ['secret'],
47
+ };
48
+
49
+ // For a person: one renderer per type, which the typecheck holds to every type in the map.
50
+ const sentences: Renderers<{ created: TokenCreated }> = {
51
+ created: (value) => `Created token ${value.id}, which expires on ${value.expiresAt.slice(0, 10)}`,
52
+ };
53
+ render(readout.data, sentences); // 'Created token tok-1, which expires on 2026-11-12'
54
+
55
+ // For a machine: the copy, and the same text embedded in the page for a driven browser.
56
+ const json = serializeReadout(readout);
57
+ const script = `<script type="application/json" id="token-data">${embedJson(json)}</script>`;
58
+ ```
59
+
60
+ ## Contract
61
+
62
+ - **Sensitive fields.** `serializeReadout` and `redactReadout` leave out each field named in `sensitive` and list it under `redacted`, so that a reader can tell a redacted field from an absent one. `{ includeSensitive: true }` keeps every field and omits `redacted`. Because `redacted` is reserved, the type rejects data with a field of that name. Only top-level fields can be marked sensitive.
63
+ - **Serialization.** `serializeReadout` returns JSON indented by two spaces. `redactReadout` returns the object that it serializes.
64
+ - **Embedding.** `embedJson` escapes every `<` in JSON text as `\u003c`, so that the content of a `<script type="application/json">` element cannot close the element or open a comment, and `JSON.parse` of the element's text returns the serialized value. It takes JSON text, not a readout, so that any serialization embeds through it.
65
+ - **Renderers.** `Renderers<M, R>` maps each type in `M`, a map from a type to the shape of its values, to a renderer that returns `R` (a string by default). `render` dispatches a value to the renderer for its `type`. JSON is not a renderer, because `serializeReadout` is the machine form.
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Returns JSON text that is safe as the content of a `<script type="application/json">` element and parses to the same
3
+ * value. In valid JSON, `<` occurs only inside a string, where its escape is equivalent, so escaping it means that the
4
+ * content can neither close the element nor open a comment.
5
+ */
6
+ export declare function embedJson(json: string): string;
@@ -0,0 +1,3 @@
1
+ export function embedJson(json) {
2
+ return json.replaceAll('<', String.raw `\u003c`);
3
+ }
@@ -0,0 +1,3 @@
1
+ export { embedJson } from './embed-json.js';
2
+ export { type Readout, type ReadoutData, redactReadout, type SerializeOptions, serializeReadout } from './readout.js';
3
+ export { render, type Renderers, type TypedValues } from './renderers.js';
@@ -0,0 +1,3 @@
1
+ export { embedJson } from "./embed-json.js";
2
+ export { redactReadout, serializeReadout } from "./readout.js";
3
+ export { render } from "./renderers.js";
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A result that a program shows to a person and also exposes as data, with the fields that the data leaves out unless
3
+ * the caller asks for them. `redacted` is reserved, because the redacted form names the fields that it leaves out under
4
+ * that key.
5
+ */
6
+ export interface Readout<T extends ReadoutData> {
7
+ data: T;
8
+ /** The fields that a serialization leaves out by default and names under `redacted`, such as a credential. */
9
+ sensitive: readonly (keyof T & string)[];
10
+ }
11
+ /** The data of a readout: an object without a field named `redacted`. */
12
+ export type ReadoutData = object & {
13
+ redacted?: never;
14
+ };
15
+ /** How a readout is serialized. */
16
+ export interface SerializeOptions {
17
+ /** Keeps every sensitive field and omits `redacted`. Off by default. */
18
+ includeSensitive?: boolean;
19
+ }
20
+ /**
21
+ * Returns the readout's data without its sensitive fields and with `redacted` naming each one that it leaves out, so
22
+ * that a reader can tell a redacted field from an absent one. With `includeSensitive`, returns every field.
23
+ */
24
+ export declare function redactReadout<T extends ReadoutData>(readout: Readout<T>, options?: SerializeOptions): Record<string, unknown>;
25
+ /** Serializes the readout as indented JSON, without its sensitive fields unless `includeSensitive` is set. */
26
+ export declare function serializeReadout<T extends ReadoutData>(readout: Readout<T>, options?: SerializeOptions): string;
@@ -0,0 +1,15 @@
1
+ export function redactReadout(readout, options = {}) {
2
+ const sensitive = new Set(options.includeSensitive === true ? [] : readout.sensitive);
3
+ const kept = {};
4
+ const redacted = [];
5
+ for (const [key, value] of Object.entries(readout.data)) {
6
+ if (sensitive.has(key))
7
+ redacted.push(key);
8
+ else
9
+ kept[key] = value;
10
+ }
11
+ return redacted.length > 0 ? { ...kept, redacted } : kept;
12
+ }
13
+ export function serializeReadout(readout, options = {}) {
14
+ return JSON.stringify(redactReadout(readout, options), null, 2);
15
+ }
@@ -0,0 +1,14 @@
1
+ /** A map from each type of value to the value's shape, in which each value names its own type. */
2
+ export type TypedValues<M> = {
3
+ [K in keyof M]: {
4
+ type: K;
5
+ };
6
+ };
7
+ /** One renderer per type in the map, so that the typecheck reports a type without one; each returns the form `R`. */
8
+ export type Renderers<M extends TypedValues<M>, R = string> = {
9
+ [K in keyof M & string]: (value: M[K]) => R;
10
+ };
11
+ /** Renders a value through the renderer for its type. */
12
+ export declare function render<M extends TypedValues<M>, K extends keyof M & string, R>(value: M[K] & {
13
+ type: K;
14
+ }, renderers: Renderers<M, R>): R;
@@ -0,0 +1,3 @@
1
+ export function render(value, renderers) {
2
+ return renderers[value.type](value);
3
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "@williamthorsen/readout",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "One result object for people and machines: redacting JSON serializer, DOM embedding, and typed renderers",
6
+ "keywords": [
7
+ "json",
8
+ "machine-readable",
9
+ "readout",
10
+ "redaction",
11
+ "renderer"
12
+ ],
13
+ "homepage": "https://github.com/williamthorsen/showroom/tree/main/packages/readout#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/williamthorsen/showroom/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/williamthorsen/showroom.git",
20
+ "directory": "packages/readout"
21
+ },
22
+ "license": "ISC",
23
+ "author": "William Thorsen <william@thorsen.dev> (https://github.com/williamthorsen)",
24
+ "sideEffects": false,
25
+ "type": "module",
26
+ "exports": {
27
+ ".": {
28
+ "import": "./dist/esm/index.js",
29
+ "types": "./dist/esm/index.d.ts"
30
+ }
31
+ },
32
+ "files": [
33
+ ".meta/changelog.json",
34
+ "CHANGELOG.md",
35
+ "dist"
36
+ ],
37
+ "devDependencies": {
38
+ "@williamthorsen/nmr": "0.54.0",
39
+ "vitest": "5.1.0"
40
+ },
41
+ "engines": {
42
+ "node": ">=24"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "typecheck:post": "tsgo --noEmit --project tsconfig.src.json"
49
+ }
6
50
  }