@yuhere/js-jsonrpc2 2.1.13 → 2.1.15

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/README.md CHANGED
@@ -72,7 +72,7 @@ The descriptor has three fields:
72
72
  | `grantTo` | `string[]` | Optional list of permissions required to call the method. |
73
73
  | `signature` | `[returnSchema, ...parameterSchema[]]` | Return type first, then parameters. |
74
74
 
75
- `signature[0]` is the return schema (`s.Schema` or `s.void()`).
75
+ `signature[0]` is the return schema (`s.Schema`, `s.void()`, or `s.stream(inner)`).
76
76
  Every later slot is either a `s.Schema` (client-supplied) or
77
77
  `s.injectable(key)` (server-injected). Injectable slots may appear at any
78
78
  position except index `0`.
@@ -97,8 +97,12 @@ repository.register(
97
97
 
98
98
  ## Schemas
99
99
 
100
- Schemas are built with the `s` factory. They validate at runtime and derive
101
- TypeScript types.
100
+ Schemas serve two roles:
101
+
102
+ 1. **Description** — document the RPC signature (`toJSON` / `system.methodSignature` / WebUI).
103
+ 2. **Validation** — check client parameters at invoke time (`parse` / `safeParse`, used by `convertParams`).
104
+
105
+ They are built with the `s` factory. They validate at runtime (unless turned off) and derive TypeScript types.
102
106
 
103
107
  ```ts
104
108
  s.string() // string
@@ -108,29 +112,134 @@ s.boolean() // boolean
108
112
  s.literal('2.0') // exact literal (string | number | boolean only)
109
113
  s.null() // exactly null
110
114
  s.array(s.string()) // string[]
111
- s.object({ id: s.number() }) // open object
112
- s.object({ id: s.number() }).strict() // all props required, no extras
115
+ s.object({ id: s.number() }) // open object (default: all props optional, extra props kept)
116
+ s.object({ id: s.number() }).strict() // all declared props required, no extras
113
117
  s.record(s.number()) // Record<string, number>
114
118
  s.union([s.string(), s.number()])
115
119
  s.tuple([s.string(), s.number()])
116
120
  s.enum(['pending', 'success', 'failed'])
117
121
  s.unknown() // any JSON value (already includes null)
118
122
  s.void() // RPC-only: no return value
123
+ s.stream(s.string()) // RPC-only: streaming return (AsyncGenerator)
119
124
  s.injectable('request') // RPC-only: server-injected parameter
120
125
  ```
121
126
 
122
- Chainable modifiers: `.min()`, `.max()`, `.nullable()`, `.strict()`,
123
- `.describe(text)`. Validation: `.parse(input)` (throws
124
- `SchemaValidationError`) and `.safeParse(input)` (returns a result object).
127
+ ### Chainable modifiers
128
+
129
+ | Modifier | Applies to | Meaning |
130
+ | --- | --- | --- |
131
+ | `.min(n)` / `.max(n)` | string (length), number (bounds), array / record (size) | Inclusive bounds. |
132
+ | `.nullable()` / `.nullable(true)` | most schemas | Allow `null`. Immutable; returns a new schema. |
133
+ | `.nullable(false)` | same | Clear a previous nullable flag. |
134
+ | `.describe(text)` | all schemas | Human-readable description (shown in WebUI / docs). |
135
+ | `.validate(false)` / `.validate(true)` | all schemas | Toggle runtime validation. Default is **on**. When off, `parse` / `safeParse` accept any value and return it unchanged; the schema still describes the type for signatures and the WebUI. |
136
+ | `.optionalProperties(keys)` | object | Whitelist of declared keys that may be missing. |
137
+ | `.allowUnknownProperties(bool)` | object | Whether undeclared keys are kept (`true`) or rejected (`false`). |
138
+ | `.strict()` / `.strict(true)` | object | Shortcut: no missing declared keys + reject extras. |
139
+ | `.strict(false)` | object | Restore the default permissive object config. |
140
+
141
+ Validation: `.parse(input)` (throws `SchemaValidationError` when validation is on)
142
+ and `.safeParse(input)` (returns a result object).
125
143
  Introspection: `.toJSON()` returns a `SchemaDescription`.
126
144
 
127
- Nullability notes:
145
+ ### Disabling validation
146
+
147
+ Schemas always participate in **signature description**. Runtime **parameter
148
+ checks** can be turned off per schema when the server trusts the caller or the
149
+ shape is only documentary:
150
+
151
+ ```ts
152
+ // Still appears as string in system.methodSignature / WebUI,
153
+ // but convertParams will not reject non-strings.
154
+ s.string().validate(false)
155
+
156
+ // Nested: only the inner schema skips checks
157
+ s.object({
158
+ id: s.number(),
159
+ payload: s.unknown().validate(false),
160
+ })
161
+ ```
162
+
163
+ - Default is `validate: true` (omitted from `toJSON()` to keep descriptions small).
164
+ - `.validate(false)` writes `validate: false` into the description.
165
+ - `.validate(true)` restores the default; the flag is not emitted.
166
+ - Calls are immutable (same pattern as `.nullable()`).
167
+
168
+ Return-type slots are not run through `convertParams`; turning validation off on
169
+ a return schema only affects direct `.parse` / `.safeParse` use and the
170
+ description flag.
171
+
172
+ ### Object defaults and strictness
173
+
174
+ By default an object schema is **permissive**:
175
+
176
+ - every declared property may be omitted;
177
+ - undeclared properties are allowed and preserved (if they are valid JSON).
178
+
179
+ ```ts
180
+ const open = s.object({ name: s.string(), age: s.number() });
181
+ open.parse({}); // {}
182
+ open.parse({ name: 'Ada', extra: 1 }); // { name: 'Ada', extra: 1 }
183
+
184
+ const closed = open.strict();
185
+ closed.parse({ name: 'Ada', age: 30 }); // ok
186
+ // missing age → error; extra keys → error
187
+ ```
188
+
189
+ Finer control without the all-or-nothing `strict()`:
190
+
191
+ ```ts
192
+ // only `age` may be missing; still allow unknown keys
193
+ s.object({ name: s.string(), age: s.number() }).optionalProperties(['age']);
194
+
195
+ // all declared keys may be missing, but reject extras
196
+ s.object({ name: s.string() }).allowUnknownProperties(false);
197
+ ```
198
+
199
+ `optionalProperties` rejects key names that are not in the shape (throws
200
+ `TypeError`).
201
+
202
+ ### Nullability notes
128
203
 
129
204
  - `.nullable()` / `.nullable(true)` allow `null`; `.nullable(false)` clears it. All calls are immutable.
130
205
  - `s.unknown()` and `s.null()` do not expose `.nullable()`; they already accept / are `null`.
131
206
  - `s.literal(v)` accepts `string | number | boolean`. Use `s.null()` for exact null, or `.nullable()` for "literal or null".
132
207
  - `s.union([...])` strips member-level nullability; only the union's own `.nullable()` decides whether `null` is accepted.
133
208
 
209
+ ### `toJSON()` / `SchemaDescription`
210
+
211
+ Descriptions are a discriminated union on `kind`. There is **no** generic
212
+ `children` bag for ordinary schemas:
213
+
214
+ | kind | Child field(s) |
215
+ | --- | --- |
216
+ | `array` | `element` (single schema) |
217
+ | `record` | `value` (single schema) |
218
+ | `object` | `properties` (map) |
219
+ | `union` | `options` (array) |
220
+ | `tuple` | `elements` (array) |
221
+
222
+ Object constraints are reported as:
223
+
224
+ ```ts
225
+ constraints: {
226
+ optionalProps: string[]; // keys allowed to be missing
227
+ allowUnknownProps: boolean; // whether extra keys are kept
228
+ }
229
+ ```
230
+
231
+ Empty constraint objects are omitted (no `constraints: {}`).
232
+
233
+ When validation is disabled, the description includes `validate: false`.
234
+ The default enabled state is not serialized.
235
+
236
+ Stream returns from `system.methodSignature` / `signatureOf` currently use a
237
+ small exception for the marker shape:
238
+
239
+ ```ts
240
+ { kind: 'stream', inner: <SchemaDescription> }
241
+ ```
242
+
134
243
  ## Transports
135
244
 
136
245
  ### Express
package/SKILL.md CHANGED
@@ -30,8 +30,26 @@ Hard constraints (by design, do not work around):
30
30
  `s.literal(v)` (`v` is `string | number | boolean` only), `s.null()`,
31
31
  `s.array(item)`, `s.object(shape)`, `s.record(v)`, `s.union([...])`,
32
32
  `s.tuple([...])`, `s.enum([...])`, `s.unknown()`.
33
- 7. Chain modifiers before registering: `.min()`, `.max()`, `.nullable(bool)`,
34
- `.strict()`, `.describe(text)`.
33
+ 7. Chain modifiers before registering:
34
+ - Bounds / docs: `.min()`, `.max()`, `.describe(text)`.
35
+ - Nullability: `.nullable()` / `.nullable(true)` / `.nullable(false)` (immutable).
36
+ - Validation gate: `.validate(true|false)` (immutable; default **on**).
37
+ - Schemas serve two roles: **describe** the RPC signature and **validate**
38
+ client params at runtime.
39
+ - `.validate(false)` keeps the schema in `signature` / `system.methodSignature`
40
+ / WebUI / docs, but `parse` / `safeParse` become no-ops (return the input
41
+ unchanged). `convertParams` therefore will not reject bad values for that
42
+ slot.
43
+ - Prefer this only when a param is intentionally untrusted-schema or
44
+ validated elsewhere; do not use it as a substitute for `s.unknown()`.
45
+ - Object shape control:
46
+ - Default object is **permissive**: every declared key may be missing;
47
+ undeclared keys are kept if they are valid JSON.
48
+ - `.optionalProperties(keys)` — whitelist of declared keys that may be
49
+ missing (unknown names throw `TypeError`).
50
+ - `.allowUnknownProperties(bool)` — keep (`true`) or reject (`false`) extras.
51
+ - `.strict()` / `.strict(true)` — no missing declared keys + reject extras.
52
+ - `.strict(false)` — restore the default permissive config.
35
53
  8. Nullability rules:
36
54
  - `.nullable()` / `.nullable(true)` allow `null`; `.nullable(false)` clears it. Calls are immutable.
37
55
  - `s.unknown()` and `s.null()` already handle `null`; they do not expose `.nullable()`.
@@ -46,6 +64,57 @@ repository.register(
46
64
  { namespace: 'add', document: 'a + b', signature: [s.number(), s.number(), s.number()] },
47
65
  (a: number, b: number) => a + b,
48
66
  );
67
+
68
+ // Describe shape for clients / WebUI, but skip runtime checks on this param:
69
+ repository.register(
70
+ {
71
+ namespace: 'legacy.passthrough',
72
+ signature: [
73
+ s.unknown(),
74
+ s.object({ id: s.number(), tags: s.array(s.string()) }).validate(false),
75
+ ],
76
+ },
77
+ (payload) => payload,
78
+ );
79
+ ```
80
+
81
+ ### Schema descriptions (`toJSON` / `system.methodSignature`)
82
+
83
+ Ordinary schemas use a discriminated union on `kind`. There is **no** generic
84
+ `children` bag:
85
+
86
+ | kind | Child field(s) |
87
+ | --- | --- |
88
+ | `array` | `element` |
89
+ | `record` | `value` |
90
+ | `object` | `properties` |
91
+ | `union` | `options` |
92
+ | `tuple` | `elements` |
93
+
94
+ Object constraints in the description:
95
+
96
+ ```ts
97
+ constraints: {
98
+ optionalProps: string[];
99
+ allowUnknownProps: boolean;
100
+ }
101
+ ```
102
+
103
+ Empty constraint objects are omitted (no `constraints: {}`).
104
+
105
+ When validation is turned off, the description includes an explicit flag
106
+ (default is on and omitted to keep noise low):
107
+
108
+ ```ts
109
+ { kind: 'object', validate: false, properties: { ... } }
110
+ ```
111
+
112
+ Do **not** read legacy paths such as `children.items` or `children.values`.
113
+
114
+ Stream returns are the exception (marker path in `signatureOf`):
115
+
116
+ ```ts
117
+ { kind: 'stream', inner: <SchemaDescription> }
49
118
  ```
50
119
 
51
120
  ## Stream a return value
@@ -205,8 +274,17 @@ Keep `withSystemMethods` on (default) to expose `system.listMethods`,
205
274
  `system.methodSignature`, `system.methodHelp`. Set `document` on a descriptor
206
275
  so `system.methodHelp` returns useful text.
207
276
 
208
- `system.methodSignature` reports a stream return as
209
- `{ kind: 'stream', children: { inner: <schema> } }`.
277
+ `system.methodSignature` reports ordinary containers with semantic fields
278
+ (`element` / `value` / `properties` / `options` / `elements`). A stream return
279
+ is reported as:
280
+
281
+ ```ts
282
+ { kind: 'stream', children: { inner: <schema> } }
283
+ ```
284
+
285
+ Schemas with `.validate(false)` appear with `validate: false` in the signature
286
+ description so clients and the WebUI can show that the server will not enforce
287
+ that slot at runtime.
210
288
 
211
289
  ## Common mistakes
212
290
 
@@ -253,3 +331,16 @@ so `system.methodHelp` returns useful text.
253
331
  - **Calling `s.literal(null)`.** `literal` only accepts `string | number |
254
332
  boolean`; construction throws `TypeError`. Use `s.null()` for exact null, or
255
333
  `.nullable()` for "literal or null".
334
+ - **Treating default `s.object(...)` as strict.** By default all declared keys
335
+ may be missing and unknown keys are kept. Use `.strict()` or
336
+ `.optionalProperties` / `.allowUnknownProperties` for tighter rules.
337
+ - **Reading schema descriptions via `children.items` / `children.values`.**
338
+ Those paths are gone. Use `element`, `value`, `properties`, `options`,
339
+ `elements`. Only stream markers still use `children.inner`.
340
+ - **Assuming `.validate(false)` still type-checks or coerces.** It only skips
341
+ runtime validation; TypeScript types and the published description stay as
342
+ declared. Use `s.unknown()` when the value truly has no schema.
343
+ - **Turning off validation on the return schema and expecting the server to
344
+ enforce the handler result.** Return schemas are for description / WebUI;
345
+ the server does not re-validate handler return values against them (except
346
+ stream declaration checks for `AsyncGenerator`).
@@ -472,4 +472,81 @@ export function installDemoMethods(repository) {
472
472
  await new Promise((r) => setTimeout(r, 100));
473
473
  }
474
474
  });
475
+ // ─────────────────────────────────────────────────────────────
476
+ // 14. validate(false):只描述、不校验
477
+ // ─────────────────────────────────────────────────────────────
478
+ // Schema 同时用于签名描述与运行时校验。默认两者都开;
479
+ // .validate(false) 后 convertParams 仍接受任意 JSON,但
480
+ // system.methodSignature / WebUI 仍展示完整类型信息。
481
+ // 14.1 对比:同一形状,一边校验一边不校验
482
+ repository.register({
483
+ namespace: 'validate.on',
484
+ document: '默认开启校验:params[0] 必须是非空字符串,否则 INVALID_PARAMS。',
485
+ signature: [
486
+ s.string().describe('回显'),
487
+ s.string().min(1).max(32).describe('非空短字符串'),
488
+ ],
489
+ }, (msg) => msg);
490
+ repository.register({
491
+ namespace: 'validate.off',
492
+ document: '与 validate.on 同形,但参数 schema 关闭了运行时校验。' +
493
+ '任意 JSON 都能进 handler;类型信息仍出现在 methodSignature / WebUI。',
494
+ signature: [
495
+ s.unknown().describe('原样返回(可能并非 string)'),
496
+ s
497
+ .string()
498
+ .min(1)
499
+ .max(32)
500
+ .describe('文档上仍是非空短字符串')
501
+ .validate(false),
502
+ ],
503
+ }, (msg) => msg);
504
+ // 14.2 对象:严格描述 + 关闭校验(信任上游已洗过数据)
505
+ repository.register({
506
+ namespace: 'validate.offObject',
507
+ document: '对象签名写了 strict,但 validate(false):缺字段 / 多余字段 / 类型错误都不会在 convertParams 阶段失败。',
508
+ signature: [
509
+ s.unknown(),
510
+ s
511
+ .object({
512
+ id: s.number().describe('用户 ID'),
513
+ name: s.string().min(1).describe('名称'),
514
+ })
515
+ .strict()
516
+ .describe('文档上的严格用户对象')
517
+ .validate(false),
518
+ ],
519
+ }, (payload) => payload);
520
+ // 14.3 嵌套:仅内层关闭校验
521
+ repository.register({
522
+ namespace: 'validate.nestedPartial',
523
+ document: '外层 array 仍校验「必须是数组」;元素 schema 关闭校验,元素内容任意。',
524
+ signature: [
525
+ s.array(s.unknown()).describe('原样返回'),
526
+ s
527
+ .array(s
528
+ .object({
529
+ code: s.string().min(1),
530
+ qty: s.number().min(0),
531
+ })
532
+ .strict()
533
+ .validate(false))
534
+ .min(1)
535
+ .describe('至少 1 个元素;元素内部不校验'),
536
+ ],
537
+ }, (items) => items);
538
+ // 14.4 可再打开:链式 validate(false).validate(true) 与默认等价(演示可撤销)
539
+ repository.register({
540
+ namespace: 'validate.reenabled',
541
+ document: '先 validate(false) 再 validate(true),效果与默认开启校验相同。',
542
+ signature: [
543
+ s.number().describe('原样数字'),
544
+ s
545
+ .number()
546
+ .min(0)
547
+ .describe('非负数字')
548
+ .validate(false)
549
+ .validate(true),
550
+ ],
551
+ }, (n) => n);
475
552
  }
@@ -50,12 +50,12 @@ function convert(desc) {
50
50
  out.minItems = c.min;
51
51
  if (c.max !== undefined)
52
52
  out.maxItems = c.max;
53
- out.items = convert(desc.children.items);
53
+ out.items = convert(desc.element);
54
54
  break;
55
55
  }
56
56
  case 'tuple': {
57
57
  out.type = 'array';
58
- const items = desc.children.items;
58
+ const items = desc.elements;
59
59
  out.items = items.map(convert);
60
60
  out.minItems = items.length;
61
61
  out.maxItems = items.length;
@@ -63,19 +63,19 @@ function convert(desc) {
63
63
  }
64
64
  case 'object': {
65
65
  out.type = 'object';
66
- const strict = Boolean(desc.constraints.strict);
67
- const props = desc.children
68
- .properties;
66
+ const optional = new Set((desc.constraints
67
+ ?.optionalProps) ?? []);
68
+ const allowUnknown = (desc.constraints
69
+ ?.allowUnknownProps) ?? true;
70
+ const props = desc.properties;
69
71
  out.properties = {};
70
72
  for (const [k, v] of Object.entries(props))
71
73
  out.properties[k] = convert(v);
72
- if (strict) {
73
- out.required = Object.keys(props);
74
+ const required = Object.keys(props).filter((k) => !optional.has(k));
75
+ if (required.length > 0)
76
+ out.required = required;
77
+ if (!allowUnknown)
74
78
  out.additionalProperties = false;
75
- }
76
- else {
77
- out.additionalProperties = true;
78
- }
79
79
  break;
80
80
  }
81
81
  case 'record': {
@@ -85,12 +85,11 @@ function convert(desc) {
85
85
  out.minProperties = c.min;
86
86
  if (c.max !== undefined)
87
87
  out.maxProperties = c.max;
88
- out.additionalProperties = convert(desc.children.values);
88
+ out.additionalProperties = convert(desc.value);
89
89
  break;
90
90
  }
91
91
  case 'union': {
92
- const options = desc.children.options;
93
- out.anyOf = options.map(convert);
92
+ out.anyOf = desc.options.map(convert);
94
93
  break;
95
94
  }
96
95
  case 'unknown': {
@@ -101,8 +100,10 @@ function convert(desc) {
101
100
  out.type = 'null';
102
101
  break;
103
102
  }
104
- default:
105
- throw new Error(`Unsupported schema kind for JSON Schema: ${desc.kind}`);
103
+ default: {
104
+ const _exhaustive = desc;
105
+ throw new Error(`Unsupported schema kind for JSON Schema: ${JSON.stringify(_exhaustive)}`);
106
+ }
106
107
  }
107
108
  if (desc.nullable) {
108
109
  if (desc.kind === 'unknown' || desc.kind === 'null') {
@@ -150,7 +151,6 @@ function renderSchema(schema, indent = 0) {
150
151
  if (c)
151
152
  head += ` \`${c}\``;
152
153
  lines.push(head);
153
- const ch = schema.children ?? {};
154
154
  const pushChild = (label, child) => {
155
155
  const sub = renderSchema(child, indent + 1);
156
156
  if (sub.length) {
@@ -158,36 +158,35 @@ function renderSchema(schema, indent = 0) {
158
158
  lines.push(...sub);
159
159
  }
160
160
  };
161
- if (isSchema(ch.inner))
162
- pushChild('inner', ch.inner);
163
- if (Array.isArray(ch.items)) {
164
- ch.items.forEach((it, i) => {
165
- if (isSchema(it))
166
- pushChild(`[${i}]`, it);
167
- });
168
- }
169
- else if (isSchema(ch.items)) {
170
- pushChild('items', ch.items);
171
- }
172
- if (Array.isArray(ch.options)) {
173
- ch.options.forEach((opt, i) => {
174
- if (isSchema(opt))
175
- pushChild(`option[${i}]`, opt);
176
- });
177
- }
178
- if (ch.properties && typeof ch.properties === 'object') {
179
- for (const [k, v] of Object.entries(ch.properties)) {
180
- if (isSchema(v))
161
+ switch (schema.kind) {
162
+ case 'array': {
163
+ pushChild('element', schema.element);
164
+ break;
165
+ }
166
+ case 'record': {
167
+ pushChild('value', schema.value);
168
+ break;
169
+ }
170
+ case 'object': {
171
+ for (const [k, v] of Object.entries(schema.properties)) {
181
172
  pushChild(k, v);
173
+ }
174
+ break;
175
+ }
176
+ case 'union': {
177
+ schema.options.forEach((opt, i) => pushChild(`option[${i}]`, opt));
178
+ break;
182
179
  }
180
+ case 'tuple': {
181
+ schema.elements.forEach((it, i) => pushChild(`[${i}]`, it));
182
+ break;
183
+ }
184
+ default:
185
+ // 叶子类型:无子节点。
186
+ break;
183
187
  }
184
- if (isSchema(ch.values))
185
- pushChild('values', ch.values);
186
188
  return lines;
187
189
  }
188
- function isSchema(v) {
189
- return !!v && typeof v === 'object' && typeof v.kind === 'string';
190
- }
191
190
  /** 渲染返回值槽位(Schema / VoidMarker / StreamMarker)。 */
192
191
  function renderReturnSlot(slot) {
193
192
  if (slot instanceof VoidMarker)
@@ -6,7 +6,7 @@ export declare const s: {
6
6
  boolean: () => import("./primitives.js").BooleanSchema;
7
7
  literal: <const T extends import("./primitives.js").LiteralValue>(value: T) => import("./primitives.js").LiteralSchema<T>;
8
8
  array: <T>(item: import("./primitives.js").Schema<T>) => import("./primitives.js").ArraySchema<T>;
9
- object: <S extends import("./primitives.js").ObjectShape>(shape: S) => import("./primitives.js").ObjectSchema<S, false>;
9
+ object: <S extends import("./primitives.js").ObjectShape>(shape: S) => import("./primitives.js").ObjectSchema<S>;
10
10
  record: <T>(value: import("./primitives.js").Schema<T>) => import("./primitives.js").RecordSchema<T>;
11
11
  union: <const S extends readonly [import("./primitives.js").Schema<unknown>, ...import("./primitives.js").Schema<unknown>[]]>(schemas: S) => import("./primitives.js").UnionSchema<S>;
12
12
  tuple: <const S extends readonly [import("./primitives.js").Schema<unknown>, ...import("./primitives.js").Schema<unknown>[]]>(schemas: S) => import("./primitives.js").TupleSchema<S>;