ts-procedures 10.1.0 → 10.1.1

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ All notable changes to **ts-procedures** are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## 10.1.1 — 2026-06-24
8
+
9
+ Fixes TypeBox schema detection for `Type.Unsafe(...)` on typebox ≥ 1.2.0. No
10
+ change to generated output, docs, or validation behavior.
11
+
12
+ ### Fixed
13
+
14
+ - **Top-level `Type.Unsafe(...)` channel schemas are recognized again.** TypeBox
15
+ 1.2.0 tags `Type.Unsafe(...)` with a `~unsafe` marker *instead of* `~kind`, so
16
+ the built-in TypeBox adapter — which only looked for `~kind` — failed to detect
17
+ a bare top-level `Type.Unsafe(...)` used as a channel schema (e.g.
18
+ `schema: { params: Type.Unsafe(...) }`), throwing a `ProcedureRegistrationError`
19
+ at registration. Detection now keys off TypeBox v1's `~`-prefix marker
20
+ *convention* (via `Reflect.ownKeys`, since the markers are non-enumerable)
21
+ rather than a fixed list of names, so current and future `~*` markers are
22
+ recognized without further changes. `Type.Unsafe` nested inside a
23
+ `Type.Object(...)` was never affected.
24
+
25
+ ### Notes
26
+
27
+ - Serialized JSON Schema was never affected: TypeBox's `~` markers are
28
+ non-enumerable, so they never reached `JSON.stringify` (DocEnvelope, codegen)
29
+ or AJV. The frozen codegen goldens and DocEnvelope parity fixtures are
30
+ unchanged. App devs only need to upgrade; no code changes required.
31
+
7
32
  ## 10.1.0 — 2026-06-24
8
33
 
9
34
  Adds explicit versioning to the DocEnvelope so downstream consumers (docs
@@ -24,9 +49,10 @@ compatible. Purely additive — existing envelopes and consumers are unaffected.
24
49
  parse-compatibility signal. Exported as `DOC_ENVELOPE_GENERATOR_VERSION` from
25
50
  `ts-procedures/server`. Strippable via the `transform` hook for servers that
26
51
  don't want to advertise the version on a public docs endpoint.
27
- - **DocEnvelope wire-contract docs** — `docs/doc-envelope-spec.md` plus a
28
- validatable JSON Schema at `docs/doc-envelope.schema.json`, for building
29
- third-party tools against the served envelope.
52
+ - **DocEnvelope wire-contract docs** — `docs/doc-envelope-spec-v1.md` plus a
53
+ validatable JSON Schema at `docs/doc-envelope-v1.schema.json` (filenames pinned
54
+ to the envelope `specVersion`), for building third-party tools against the
55
+ served envelope.
30
56
 
31
57
  ### Internal
32
58
 
@@ -2,7 +2,7 @@ import type { SchemaAdapter } from './adapter.js';
2
2
  /**
3
3
  * Built-in adapter for TypeBox (`import { Type } from 'typebox'`).
4
4
  *
5
- * Detection covers both typebox v1 (`~kind` property) and legacy
5
+ * Detection covers both typebox v1 (any `~`-prefixed marker) and legacy
6
6
  * `@sinclair/typebox` 0.3x (`Symbol.for('TypeBox.Kind')`). TypeBox schemas
7
7
  * already ARE JSON Schema, so conversion is the identity.
8
8
  */
@@ -1,8 +1,26 @@
1
1
  const TYPEBOX_KIND = Symbol.for('TypeBox.Kind');
2
+ /** TypeBox v1 prefixes every internal marker key with `~`. */
3
+ const TILDE = '~'.charCodeAt(0);
4
+ /**
5
+ * TypeBox v1 namespaces all of its internal markers under a `~` prefix
6
+ * (`~kind`, `~unsafe`, `~optional`, ...) — non-enumerable own properties that
7
+ * stay out of the serialized JSON Schema. Detecting the *convention* rather
8
+ * than specific marker names means new markers (e.g. `~unsafe`, added in
9
+ * typebox 1.2.0 and used by `Type.Unsafe(...)` *instead of* `~kind`) are
10
+ * recognized without a code change. Reflect.ownKeys is required because these
11
+ * markers are non-enumerable.
12
+ */
13
+ function hasTypeBoxV1Marker(schema) {
14
+ for (const key of Reflect.ownKeys(schema)) {
15
+ if (typeof key === 'string' && key.charCodeAt(0) === TILDE)
16
+ return true;
17
+ }
18
+ return false;
19
+ }
2
20
  /**
3
21
  * Built-in adapter for TypeBox (`import { Type } from 'typebox'`).
4
22
  *
5
- * Detection covers both typebox v1 (`~kind` property) and legacy
23
+ * Detection covers both typebox v1 (any `~`-prefixed marker) and legacy
6
24
  * `@sinclair/typebox` 0.3x (`Symbol.for('TypeBox.Kind')`). TypeBox schemas
7
25
  * already ARE JSON Schema, so conversion is the identity.
8
26
  */
@@ -11,7 +29,7 @@ export const typeboxAdapter = {
11
29
  detect(schema) {
12
30
  if (typeof schema !== 'object' || schema === null)
13
31
  return false;
14
- return '~kind' in schema || TYPEBOX_KIND in schema;
32
+ return hasTypeBoxV1Marker(schema) || TYPEBOX_KIND in schema;
15
33
  },
16
34
  toJsonSchema(schema) {
17
35
  return schema;
@@ -1 +1 @@
1
- {"version":3,"file":"typebox.js","sourceRoot":"","sources":["../../src/schema/typebox.ts"],"names":[],"mappings":"AAGA,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;AAE/C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAkB;IAC3C,IAAI,EAAE,SAAS;IACf,MAAM,CAAC,MAAe;QACpB,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,KAAK,CAAA;QAC/D,OAAO,OAAO,IAAI,MAAM,IAAI,YAAY,IAAI,MAAM,CAAA;IACpD,CAAC;IACD,YAAY,CAAC,MAAe;QAC1B,OAAO,MAAqB,CAAA;IAC9B,CAAC;CACF,CAAA;AAED,4DAA4D;AAC5D,MAAM,UAAU,eAAe,CAAC,MAAe;IAC7C,OAAO,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AACtC,CAAC"}
1
+ {"version":3,"file":"typebox.js","sourceRoot":"","sources":["../../src/schema/typebox.ts"],"names":[],"mappings":"AAGA,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAA;AAE/C,8DAA8D;AAC9D,MAAM,KAAK,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;AAE/B;;;;;;;;GAQG;AACH,SAAS,kBAAkB,CAAC,MAAc;IACxC,KAAK,MAAM,GAAG,IAAI,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK;YAAE,OAAO,IAAI,CAAA;IACzE,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,cAAc,GAAkB;IAC3C,IAAI,EAAE,SAAS;IACf,MAAM,CAAC,MAAe;QACpB,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,KAAK,CAAA;QAC/D,OAAO,kBAAkB,CAAC,MAAM,CAAC,IAAI,YAAY,IAAI,MAAM,CAAA;IAC7D,CAAC;IACD,YAAY,CAAC,MAAe;QAC1B,OAAO,MAAqB,CAAA;IAC9B,CAAC;CACF,CAAA;AAED,4DAA4D;AAC5D,MAAM,UAAU,eAAe,CAAC,MAAe;IAC7C,OAAO,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;AACtC,CAAC"}
@@ -6,6 +6,16 @@ describe('typebox schema detection', () => {
6
6
  test('it recognizes TypeBox schema', () => {
7
7
  expect(isTypeboxSchema(typebox)).toBe(true);
8
8
  });
9
+ test('it recognizes a top-level Type.Unsafe schema (~unsafe marker, no ~kind)', () => {
10
+ // typebox >= 1.2.0: Type.Unsafe carries `~unsafe` INSTEAD of `~kind`.
11
+ expect(isTypeboxSchema(Type.Unsafe({ type: 'string' }))).toBe(true);
12
+ });
13
+ test('it recognizes any future ~-prefixed marker (convention, not a name list)', () => {
14
+ // Detection keys off TypeBox's `~` namespace convention, so a marker that
15
+ // doesn't exist yet is still recognized without a code change.
16
+ const futureMarker = { '~somethingNew': true, type: 'object' };
17
+ expect(isTypeboxSchema(futureMarker)).toBe(true);
18
+ });
9
19
  test('it rejects non-TypeBox values', () => {
10
20
  expect(isTypeboxSchema({ type: 'object' })).toBe(false);
11
21
  expect(isTypeboxSchema('string value')).toBe(false);
@@ -1 +1 @@
1
- {"version":3,"file":"typebox.test.js","sourceRoot":"","sources":["../../src/schema/typebox.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAC/C,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAC9B,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAE9D,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAEpD,IAAI,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACxC,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,eAAe,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACvD,MAAM,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnD,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACxC,MAAM,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACzC,MAAM,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAChD,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,kEAAkE,EAAE,GAAG,EAAE;QAC5E,MAAM,MAAM,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAA;QACzE,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,IAAI,CAAC,yDAAyD,EAAE,GAAG,EAAE;YACnE,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QAC7C,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,gCAAgC,EAAE,GAAG,EAAE;YAC1C,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YACjD,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;YAC7D,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,CAAC,CAAC,CAAA;QAEF,IAAI,CAAC,gEAAgE,EAAE,GAAG,EAAE;YAC1E,MAAM,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC5D,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"typebox.test.js","sourceRoot":"","sources":["../../src/schema/typebox.test.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,9 @@
1
- # ts-procedures DocEnvelope — Consumer Spec
1
+ # ts-procedures DocEnvelope — Consumer Spec (`specVersion: 1`)
2
+
3
+ > **This document describes DocEnvelope `specVersion: 1`.** The filename is
4
+ > pinned to the spec version (`-v1`) so future format changes land as a sibling
5
+ > (`doc-envelope-spec-v2.md`) and the differences stay diffable. Check the
6
+ > envelope's `specVersion` field (absent ⇒ `1`) to know which spec applies.
2
7
 
3
8
  A spec for a GUI that fetches a `DocEnvelope` JSON document from a ts-procedures
4
9
  server URL and renders it. This is the **stable wire contract** between a
@@ -6,7 +11,7 @@ ts-procedures server and any documentation/codegen consumer. It is a *frozen*
6
11
  contract in the framework (changing field names/nesting breaks codegen goldens),
7
12
  so you can build against it confidently.
8
13
 
9
- Companion file: `doc-envelope.schema.json` (JSON Schema Draft-07) — validate
14
+ Companion file: `doc-envelope-v1.schema.json` (JSON Schema Draft-07) — validate
10
15
  fetched documents against it.
11
16
 
12
17
  ---
@@ -26,7 +31,7 @@ So your website's "paste a URL" flow should:
26
31
  what the official codegen uses as its "is this an envelope?" gate. If
27
32
  `routes` is missing/not-an-array, show a clear "not a ts-procedures docs
28
33
  document" error.
29
- 4. Optionally validate fully against `doc-envelope.schema.json`.
34
+ 4. Optionally validate fully against `doc-envelope-v1.schema.json`.
30
35
 
31
36
  CORS: the server must allow your origin, or you'll need a proxy. Worth surfacing
32
37
  as a distinct error from "bad JSON".
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
- "$id": "https://ts-procedures.dev/schemas/doc-envelope.schema.json",
4
- "title": "ts-procedures DocEnvelope",
3
+ "$id": "https://ts-procedures.dev/schemas/doc-envelope-v1.schema.json",
4
+ "title": "ts-procedures DocEnvelope (specVersion 1)",
5
5
  "description": "The JSON document a ts-procedures server exposes (via builder.toDocEnvelope() / DocRegistry.toJSON()). Describes every procedure/route the server serves, the global error catalog, and global headers. Payload schemas embedded under each route's `jsonSchema` are arbitrary JSON Schema (Draft-07) objects and are intentionally left unconstrained here.",
6
6
  "type": "object",
7
7
  "required": ["basePath", "headers", "errors", "routes"],
@@ -337,7 +337,7 @@ app.get('/docs', (c) => c.json(
337
337
  ))
338
338
  ```
339
339
 
340
- The `DocRegistry` output is the input for [Client Code Generation](./client-and-codegen.md). Building a third-party docs viewer or other tool that consumes the served envelope? The full wire contract — every field, per-kind shapes, and a validatable JSON Schema — is in [DocEnvelope Spec](./doc-envelope-spec.md) (`doc-envelope.schema.json`).
340
+ The `DocRegistry` output is the input for [Client Code Generation](./client-and-codegen.md). Building a third-party docs viewer or other tool that consumes the served envelope? The full wire contract — every field, per-kind shapes, and a validatable JSON Schema — is in [DocEnvelope Spec](./doc-envelope-spec-v1.md) (`doc-envelope-v1.schema.json`).
341
341
 
342
342
  ## Type Exports
343
343
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-procedures",
3
- "version": "10.1.0",
3
+ "version": "10.1.1",
4
4
  "description": "A TypeScript RPC framework that creates type-safe, schema-validated procedure calls with a single function definition. Define your procedures once and get full type inference, runtime validation, and framework integration hooks.",
5
5
  "main": "build/exports.js",
6
6
  "types": "build/exports.d.ts",
@@ -9,6 +9,18 @@ describe('typebox schema detection', () => {
9
9
  expect(isTypeboxSchema(typebox)).toBe(true)
10
10
  })
11
11
 
12
+ test('it recognizes a top-level Type.Unsafe schema (~unsafe marker, no ~kind)', () => {
13
+ // typebox >= 1.2.0: Type.Unsafe carries `~unsafe` INSTEAD of `~kind`.
14
+ expect(isTypeboxSchema(Type.Unsafe({ type: 'string' }))).toBe(true)
15
+ })
16
+
17
+ test('it recognizes any future ~-prefixed marker (convention, not a name list)', () => {
18
+ // Detection keys off TypeBox's `~` namespace convention, so a marker that
19
+ // doesn't exist yet is still recognized without a code change.
20
+ const futureMarker = { '~somethingNew': true, type: 'object' }
21
+ expect(isTypeboxSchema(futureMarker)).toBe(true)
22
+ })
23
+
12
24
  test('it rejects non-TypeBox values', () => {
13
25
  expect(isTypeboxSchema({ type: 'object' })).toBe(false)
14
26
  expect(isTypeboxSchema('string value')).toBe(false)
@@ -3,10 +3,29 @@ import type { TJSONSchema } from './json-schema.js'
3
3
 
4
4
  const TYPEBOX_KIND = Symbol.for('TypeBox.Kind')
5
5
 
6
+ /** TypeBox v1 prefixes every internal marker key with `~`. */
7
+ const TILDE = '~'.charCodeAt(0)
8
+
9
+ /**
10
+ * TypeBox v1 namespaces all of its internal markers under a `~` prefix
11
+ * (`~kind`, `~unsafe`, `~optional`, ...) — non-enumerable own properties that
12
+ * stay out of the serialized JSON Schema. Detecting the *convention* rather
13
+ * than specific marker names means new markers (e.g. `~unsafe`, added in
14
+ * typebox 1.2.0 and used by `Type.Unsafe(...)` *instead of* `~kind`) are
15
+ * recognized without a code change. Reflect.ownKeys is required because these
16
+ * markers are non-enumerable.
17
+ */
18
+ function hasTypeBoxV1Marker(schema: object): boolean {
19
+ for (const key of Reflect.ownKeys(schema)) {
20
+ if (typeof key === 'string' && key.charCodeAt(0) === TILDE) return true
21
+ }
22
+ return false
23
+ }
24
+
6
25
  /**
7
26
  * Built-in adapter for TypeBox (`import { Type } from 'typebox'`).
8
27
  *
9
- * Detection covers both typebox v1 (`~kind` property) and legacy
28
+ * Detection covers both typebox v1 (any `~`-prefixed marker) and legacy
10
29
  * `@sinclair/typebox` 0.3x (`Symbol.for('TypeBox.Kind')`). TypeBox schemas
11
30
  * already ARE JSON Schema, so conversion is the identity.
12
31
  */
@@ -14,7 +33,7 @@ export const typeboxAdapter: SchemaAdapter = {
14
33
  name: 'typebox',
15
34
  detect(schema: unknown): boolean {
16
35
  if (typeof schema !== 'object' || schema === null) return false
17
- return '~kind' in schema || TYPEBOX_KIND in schema
36
+ return hasTypeBoxV1Marker(schema) || TYPEBOX_KIND in schema
18
37
  },
19
38
  toJsonSchema(schema: unknown): TJSONSchema {
20
39
  return schema as TJSONSchema