paramour 0.1.0 → 0.2.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/dist/errors.d.ts CHANGED
@@ -31,10 +31,28 @@ export declare class SearchDecodeError extends ParamourError {
31
31
  constructor(issues: readonly Issue[]);
32
32
  static [Symbol.hasInstance](value: unknown): value is SearchDecodeError;
33
33
  }
34
+ /**
35
+ * A search source violated its wire-shape contract (design-08 STD7): a
36
+ * non-object source, or a non-string / non-string[] value under a read key.
37
+ * Thrown by search.ts's source readers; distinct from {@link ParamourError}
38
+ * so the Standard Schema adapter can soften exactly these throws to issues
39
+ * while config-contract violations and rebranded validator throws stay loud.
40
+ */
41
+ export declare class SearchSourceError extends ParamourError {
42
+ /** The offending source key, or null when the source itself is malformed. */
43
+ readonly key: null | string;
44
+ constructor(message: string, key: null | string);
45
+ static [Symbol.hasInstance](value: unknown): value is SearchSourceError;
46
+ }
34
47
  /** A value could not be serialized to the wire (bad type, non-finite, etc.). */
35
48
  export declare class SerializeError extends ParamourError {
36
49
  static [Symbol.hasInstance](value: unknown): value is SerializeError;
37
50
  }
51
+ /**
52
+ * Renders a value's type for "…, got X" error messages, distinguishing null
53
+ * from typeof's "object". Not exported from the package.
54
+ */
55
+ export declare function describeType(value: unknown): string;
38
56
  /**
39
57
  * Best-effort human-readable message for a foreign (non-paramour) throw.
40
58
  * Not exported from the package — internal to error branding.
package/dist/errors.js CHANGED
@@ -11,6 +11,7 @@ const paramourErrorBrand = Symbol.for("paramour.errors.ParamourError");
11
11
  const paramsDecodeErrorBrand = Symbol.for("paramour.errors.ParamsDecodeError");
12
12
  const parseErrorBrand = Symbol.for("paramour.errors.ParseError");
13
13
  const searchDecodeErrorBrand = Symbol.for("paramour.errors.SearchDecodeError");
14
+ const searchSourceErrorBrand = Symbol.for("paramour.errors.SearchSourceError");
14
15
  const serializeErrorBrand = Symbol.for("paramour.errors.SerializeError");
15
16
  /** Base class for every error paramour throws. */
16
17
  export class ParamourError extends Error {
@@ -68,6 +69,27 @@ export class SearchDecodeError extends ParamourError {
68
69
  return hasBrand(value, searchDecodeErrorBrand);
69
70
  }
70
71
  }
72
+ /**
73
+ * A search source violated its wire-shape contract (design-08 STD7): a
74
+ * non-object source, or a non-string / non-string[] value under a read key.
75
+ * Thrown by search.ts's source readers; distinct from {@link ParamourError}
76
+ * so the Standard Schema adapter can soften exactly these throws to issues
77
+ * while config-contract violations and rebranded validator throws stay loud.
78
+ */
79
+ export class SearchSourceError extends ParamourError {
80
+ static {
81
+ brandPrototype(this, searchSourceErrorBrand);
82
+ }
83
+ /** The offending source key, or null when the source itself is malformed. */
84
+ key;
85
+ constructor(message, key) {
86
+ super(message);
87
+ this.key = key;
88
+ }
89
+ static [Symbol.hasInstance](value) {
90
+ return hasBrand(value, searchSourceErrorBrand);
91
+ }
92
+ }
71
93
  /** A value could not be serialized to the wire (bad type, non-finite, etc.). */
72
94
  export class SerializeError extends ParamourError {
73
95
  static {
@@ -77,6 +99,13 @@ export class SerializeError extends ParamourError {
77
99
  return hasBrand(value, serializeErrorBrand);
78
100
  }
79
101
  }
102
+ /**
103
+ * Renders a value's type for "…, got X" error messages, distinguishing null
104
+ * from typeof's "object". Not exported from the package.
105
+ */
106
+ export function describeType(value) {
107
+ return value === null ? "null" : typeof value;
108
+ }
80
109
  /**
81
110
  * Best-effort human-readable message for a foreign (non-paramour) throw.
82
111
  * Not exported from the package — internal to error branding.
package/dist/href.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AnyRoute } from "./route.js";
1
+ import type { AnyRoute, RegisteredStaticRoutePaths } from "./route.js";
2
2
  import { type InferParamsInput } from "./path.js";
3
3
  import { type SearchInputOf } from "./search.js";
4
4
  /**
@@ -35,6 +35,19 @@ export type HrefArgs<R extends AnyRoute> = Record<never, never> extends InferHre
35
35
  export type InferHrefInput<R extends AnyRoute> = PartFor<"params", InferParamsInput<R>> & PartFor<"search", SearchInputOf<R["~search"]>> & {
36
36
  hash?: string;
37
37
  };
38
+ /**
39
+ * The string form's options (SH4): hash only. `params` is meaningless on a
40
+ * static path, and a query string comes only from a defined route's search
41
+ * codecs — a raw-search escape hatch here would be an untyped side door
42
+ * around library-owned serialization. Both are banned outright (`?: never`,
43
+ * the 2026-07-04 ruling's move) rather than merely omitted, so a non-fresh
44
+ * options object can't smuggle them past excess-property checking.
45
+ */
46
+ export interface StaticHrefOptions {
47
+ hash?: string;
48
+ params?: never;
49
+ search?: never;
50
+ }
38
51
  /**
39
52
  * One options property whose presence follows its input type: required iff
40
53
  * the input has at least one required key (the design-02 D4
@@ -54,6 +67,13 @@ type PartFor<Key extends string, Input> = keyof Input extends never ? Partial<Re
54
67
  * (a missing param codec or `~search` config) are base `ParamourError` — a
55
68
  * JS caller omitting a required `search` half falls through to
56
69
  * encodeSearch's own required-missing error.
70
+ *
71
+ * The string form (SH1): a registered STATIC path stands in for the route
72
+ * object — same brand, same hash assembly, no route definition needed. The
73
+ * string overload sits first so the route-object overload is last (SH8):
74
+ * TS's "the last overload gave the following error" heuristic keeps
75
+ * route-object misuse diagnostics prominent.
57
76
  */
77
+ export declare function href<P extends RegisteredStaticRoutePaths>(path: P, options?: StaticHrefOptions): Href<P>;
58
78
  export declare function href<R extends AnyRoute>(route: R, ...args: HrefArgs<R>): Href<R["path"]>;
59
79
  export {};
package/dist/href.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { ParamourError } from "./errors.js";
1
2
  import { buildPath } from "./path.js";
2
3
  import { searchToString, } from "./search.js";
3
4
  // The conditional HrefArgs tuple is unresolvable inside a generic body, so
@@ -5,12 +6,27 @@ import { searchToString, } from "./search.js";
5
6
  // routeData's config cast) instead of per-expression casts: the
6
7
  // implementation sees each option half at its loosest honest type.
7
8
  export function href(route, options) {
8
- const path = buildPath(route, options?.params ?? {});
9
- const query = searchToString(route["~search"], options?.search ?? {});
10
9
  // S10: the fragment is appended VERBATIM — no encoding, the caller owns
11
10
  // escaping (a value already starting with "#" yields "##…"). The empty
12
11
  // string emits no "#".
13
12
  const hash = options?.hash;
14
13
  const fragment = hash === undefined || hash === "" ? "" : `#${hash}`;
14
+ if (typeof route === "string") {
15
+ // SH6: fail-fast backstop for JS callers and world-A typos of the
16
+ // dynamic-path variety — a bracket means "you need a route object", and
17
+ // query/hash never ride in the path string (query comes only from
18
+ // search codecs, hash only from the option).
19
+ if (!route.startsWith("/") || /[#?[\]]/.test(route)) {
20
+ throw new ParamourError(`href(path) requires a static route path, got ${JSON.stringify(route)}: dynamic segments need a route object, and query/hash never ride in the path string`);
21
+ }
22
+ // SH6: silently dropping a half a JS caller passed would build a wrong
23
+ // link — contract violations stay loud (never the safe-parse error arm).
24
+ if (options?.params !== undefined || options?.search !== undefined) {
25
+ throw new ParamourError(`href(path) takes no params/search — a static path has no params, and a query string needs a route with search codecs`);
26
+ }
27
+ return `${route}${fragment}`;
28
+ }
29
+ const path = buildPath(route, options?.params ?? {});
30
+ const query = searchToString(route["~search"], options?.search ?? {});
15
31
  return `${path}${query}${fragment}`;
16
32
  }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  export { type AnyCodec, type Arity, type Codec, type OutputOf, type ParamCodec, type Presence, type PresenceOf, } from "./codec.js";
2
2
  export { type CodecDefaultDescription, type CodecDescription, describeCodec, describeRoute, type ParamDescription, type RouteDescription, type SearchDescription, } from "./describe.js";
3
- export { type Issue, ParamourError, ParamsDecodeError, ParseError, type RouteDecodeError, SearchDecodeError, SerializeError, } from "./errors.js";
4
- export { href, type Href, type HrefArgs, type InferHrefInput } from "./href.js";
3
+ export { type Issue, ParamourError, ParamsDecodeError, ParseError, type RouteDecodeError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
4
+ export { href, type Href, type HrefArgs, type InferHrefInput, type StaticHrefOptions, } from "./href.js";
5
5
  export { p } from "./p.js";
6
6
  export { buildPath, decodeParams, type DecodeParamsOptions, encodeParams, encodeStaticParams, type InferStaticParams, type ParamsSource, } from "./path.js";
7
- export { type AnyAppRoute, type AnyPagesRoute, type AnyRoute, type AppRoute, defineAppRoute, definePagesRoute, type InferRouteParams, type PagesContext, type PagesRoute, type ParamourRegister, type ParamsConfig, type ParamsProps, type RegisteredAppRoutePaths, type RegisteredPagesRoutePaths, type Route, type RouteProps, type RouterKind, type SafeResult, type SearchProps, } from "./route.js";
7
+ export { type AnyAppRoute, type AnyPagesRoute, type AnyRoute, type AppRoute, defineAppRoute, definePagesRoute, type InferRouteParams, type PagesContext, type PagesRoute, type ParamourRegister, type ParamsConfig, type ParamsProps, type RegisteredAppRoutePaths, type RegisteredPagesRoutePaths, type RegisteredStaticAppRoutePaths, type RegisteredStaticPagesRoutePaths, type RegisteredStaticRoutePaths, type Route, type RouteProps, type RouterKind, type SafeResult, type SearchProps, } from "./route.js";
8
8
  export { safeDecodeParams, safeDecodeSearch } from "./safe-decode.js";
9
9
  export { buildSearchString, decodeSearch, encodeSearch, type InferSearchInput, type InferSearchOutput, rawSearch, type RawSearch, type SearchConfig, type SearchOutputOf, type SearchSource, searchToString, } from "./search.js";
10
+ export { standardSearchSchema, type StandardSearchSchema, } from "./standard-schema.js";
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  export {} from "./codec.js";
2
2
  export { describeCodec, describeRoute, } from "./describe.js";
3
- export { ParamourError, ParamsDecodeError, ParseError, SearchDecodeError, SerializeError, } from "./errors.js";
4
- export { href } from "./href.js";
3
+ export { ParamourError, ParamsDecodeError, ParseError, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
4
+ export { href, } from "./href.js";
5
5
  export { p } from "./p.js";
6
6
  export { buildPath, decodeParams, encodeParams, encodeStaticParams, } from "./path.js";
7
7
  export { defineAppRoute, definePagesRoute, } from "./route.js";
8
8
  export { safeDecodeParams, safeDecodeSearch } from "./safe-decode.js";
9
9
  export { buildSearchString, decodeSearch, encodeSearch, rawSearch, searchToString, } from "./search.js";
10
+ export { standardSearchSchema, } from "./standard-schema.js";
package/dist/path.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ParamourError, ParamsDecodeError, ParseError, SerializeError, } from "./errors.js";
1
+ import { describeType, ParamourError, ParamsDecodeError, ParseError, SerializeError, } from "./errors.js";
2
2
  import { encodeComponent, readInputValue } from "./search.js";
3
3
  // Anchored per the wire-format spec's regex ethos; name charset excludes
4
4
  // brackets so nesting can't smuggle through. Match order mirrors the type
@@ -32,7 +32,7 @@ export function decodeParams(route, source, options) {
32
32
  // a per-key decode issue.
33
33
  const untrusted = source;
34
34
  if (typeof untrusted !== "object" || untrusted === null) {
35
- throw new ParamourError(`params source must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
35
+ throw new ParamourError(`params source must be an object, got ${describeType(untrusted)}`);
36
36
  }
37
37
  // R5: App-Router surfaces arrive percent-encoded (decode by default); pages
38
38
  // surfaces arrive already-decoded and opt out via `{ percentDecode: false }`.
@@ -182,7 +182,7 @@ export function encodeParams(route, params) {
182
182
  // here; a null input must fail loud, not read as every-param-absent.
183
183
  const untrusted = params;
184
184
  if (typeof untrusted !== "object" || untrusted === null) {
185
- throw new SerializeError(`params input must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
185
+ throw new SerializeError(`params input must be an object, got ${describeType(untrusted)}`);
186
186
  }
187
187
  const values = untrusted;
188
188
  const config = route["~params"];
@@ -232,7 +232,7 @@ export function encodeStaticParams(route, params) {
232
232
  // here; a null input must fail loud, not read as every-param-absent.
233
233
  const untrusted = params;
234
234
  if (typeof untrusted !== "object" || untrusted === null) {
235
- throw new SerializeError(`params input must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
235
+ throw new SerializeError(`params input must be an object, got ${describeType(untrusted)}`);
236
236
  }
237
237
  const values = untrusted;
238
238
  const config = route["~params"];
package/dist/route.d.ts CHANGED
@@ -163,6 +163,27 @@ export type RegisteredAppRoutePaths = ParamourRegister extends {
163
163
  export type RegisteredPagesRoutePaths = ParamourRegister extends {
164
164
  pagesRoutes: infer R extends string;
165
165
  } ? R : string;
166
+ /**
167
+ * Static-only subset of {@link RegisteredAppRoutePaths} (SH2): derived by
168
+ * syntactic filter, not emitted — dynamic-ness is a property of the path
169
+ * literal, so the registry format doesn't change. Same world-A `string`
170
+ * fallback as the full union (the filter passes `string` through).
171
+ */
172
+ export type RegisteredStaticAppRoutePaths = StaticPathsOf<RegisteredAppRoutePaths>;
173
+ /** Pages twin of {@link RegisteredStaticAppRoutePaths} (SH2). */
174
+ export type RegisteredStaticPagesRoutePaths = StaticPathsOf<RegisteredPagesRoutePaths>;
175
+ /**
176
+ * Every registered STATIC path across both routers — the string form of
177
+ * href's path argument (SH1/SH2). Deliberately NOT the union of the two
178
+ * per-router types (SH3): each falls back to `string` when its registry
179
+ * member is absent, and in a single-router project the absent side's
180
+ * `string` would swallow the union and erase verification for the router
181
+ * that HAS routes. The permissive fallback applies only when NEITHER member
182
+ * is present (world A / TR3's empty merge).
183
+ */
184
+ export type RegisteredStaticRoutePaths = [PresentRegisteredPaths] extends [
185
+ never
186
+ ] ? string : StaticPathsOf<PresentRegisteredPaths>;
166
187
  /**
167
188
  * The router-agnostic core of a defined route (PR3): path, configs, and the
168
189
  * define-time token cache. The parse surface is router-specific and lives on
@@ -234,6 +255,24 @@ export type Segments<S extends string> = S extends `${infer Head}/${infer Rest}`
234
255
  * `[...slug]` would extract as a single param named `"...slug"`.
235
256
  */
236
257
  export type SingleParamNames<Path extends string> = Segments<Path> extends infer S extends string ? S extends `[[...${string}]]` ? never : S extends `[...${string}]` ? never : S extends `[${infer Name}]` ? NonEmptyName<Name> : never : never;
258
+ /**
259
+ * Union of the registry members that are actually PRESENT — `never` when
260
+ * neither router has generated routes. The input to SH3's combined-union
261
+ * fallback rule; see {@link RegisteredStaticRoutePaths}.
262
+ */
263
+ type PresentRegisteredPaths = (ParamourRegister extends {
264
+ appRoutes: infer A extends string;
265
+ } ? A : never) | (ParamourRegister extends {
266
+ pagesRoutes: infer P extends string;
267
+ } ? P : never);
268
+ /**
269
+ * Filters a path union to its static members (SH2): any `[` marks a dynamic
270
+ * segment. `string` passes through (it doesn't extend the bracket template),
271
+ * which is exactly what keeps the world-A fallback intact. Note reachability
272
+ * ≠ staticness (SH7): `/docs/[[...slug]]` serves `/docs`, but it carries a
273
+ * codec and decode expectations, so it is excluded here.
274
+ */
275
+ type StaticPathsOf<P extends string> = P extends `${string}[${string}` ? never : P;
237
276
  /**
238
277
  * Defines an App Router route: the URL-shaped path literal (RL2) plus its
239
278
  * param/search codec configs. Validates the literal eagerly (RL1 —
@@ -251,3 +290,4 @@ export declare function defineAppRoute<Path extends RegisteredAppRoutePaths & st
251
290
  * sync context pair (PR10).
252
291
  */
253
292
  export declare function definePagesRoute<Path extends RegisteredPagesRoutePaths & string, const PC extends ParamsConfig<Path> = ParamsConfig<Path>, const SC extends Readonly<Partial<Record<PathParamNames<Path>, never>>> & SearchSlot = Record<never, never>>(path: Path, config: RouteConfig<Path, PC, SC>): PagesRoute<Path, PC, SC>;
293
+ export {};
package/dist/route.js CHANGED
@@ -1,4 +1,4 @@
1
- import { foreignMessage, ParamourError, ParamsDecodeError, SearchDecodeError, } from "./errors.js";
1
+ import { describeType, foreignMessage, ParamourError, ParamsDecodeError, SearchDecodeError, } from "./errors.js";
2
2
  import { decodeParams, tokenizePath, } from "./path.js";
3
3
  import { decodeSearch, } from "./search.js";
4
4
  /**
@@ -199,12 +199,12 @@ async function safely(run) {
199
199
  function splitPagesContext(context, paramNames) {
200
200
  const untrusted = context;
201
201
  if (typeof untrusted !== "object" || untrusted === null) {
202
- throw new ParamourError(`pages context must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
202
+ throw new ParamourError(`pages context must be an object, got ${describeType(untrusted)}`);
203
203
  }
204
204
  const { params, query } = untrusted;
205
205
  const untrustedQuery = query;
206
206
  if (typeof untrustedQuery !== "object" || untrustedQuery === null) {
207
- throw new ParamourError(`pages context has no query object (got ${untrustedQuery === null ? "null" : typeof untrustedQuery}): getStaticProps contexts carry no query string — decode ctx.params with safeDecodeParams instead (PR10)`);
207
+ throw new ParamourError(`pages context has no query object (got ${describeType(untrustedQuery)}): getStaticProps contexts carry no query string — decode ctx.params with safeDecodeParams instead (PR10)`);
208
208
  }
209
209
  return [params ?? pickOwn(query, paramNames), omitOwn(query, paramNames)];
210
210
  }
package/dist/search.d.ts CHANGED
@@ -111,6 +111,13 @@ export declare function encodeComponent(text: string): string;
111
111
  * record goes straight to the byte layer and the schema never runs on encode.
112
112
  */
113
113
  export declare function encodeSearch<S extends SearchSlot>(config: S, input: SearchInputOf<S>): [string, string][];
114
+ /**
115
+ * Runtime discriminant for the `search:` slot (design-04 SS2): the reserved
116
+ * `~kind` marker is unambiguous against a codec map, which never carries a
117
+ * top-level `~`-prefixed key. Module-exported for standard-schema.ts, not
118
+ * barrel-exported.
119
+ */
120
+ export declare function isRawSearch(config: SearchSlot): config is RawSearch<StandardSchemaV1>;
114
121
  /**
115
122
  * The whole-object search escape hatch (design-04 SS1, maintainer ruling):
116
123
  * an explicit, greppable wrapper around a bare Standard Schema so a route's
@@ -134,6 +141,15 @@ export declare function rawSearch<S extends StandardSchemaV1>(schema: S): RawSea
134
141
  * members too.
135
142
  */
136
143
  export declare function readInputValue(values: Record<string, unknown>, key: string): unknown;
144
+ /**
145
+ * The TS contract makes a non-object config unrepresentable, but a
146
+ * hand-built route missing `~search` reaches both codecs' entry points via
147
+ * href/parseSearch in plain JS; fail branded — a missing config is a
148
+ * config-contract violation (requireCodec's precedent), never a raw
149
+ * TypeError out of Object.entries/Object.keys. Module-exported for
150
+ * standard-schema.ts, not barrel-exported.
151
+ */
152
+ export declare function requireSearchConfig(config: SearchSlot): void;
137
153
  /** Convenience: encode + build in one step. */
138
154
  export declare function searchToString<S extends SearchSlot>(config: S, input: SearchInputOf<S>): string;
139
155
  export {};
package/dist/search.js CHANGED
@@ -1,4 +1,4 @@
1
- import { foreignMessage, ParamourError, ParseError, rebrandForeign, SearchDecodeError, SerializeError, } from "./errors.js";
1
+ import { describeType, foreignMessage, ParamourError, ParseError, rebrandForeign, SearchDecodeError, SearchSourceError, SerializeError, } from "./errors.js";
2
2
  import { runStandardSchemaSync } from "./schema.js";
3
3
  /**
4
4
  * Builds the byte-layer query string from decoded pairs. Hand-rolled on
@@ -147,7 +147,7 @@ export function encodeSearch(config, input) {
147
147
  // here; a null input must fail loud, not read as every-key-absent.
148
148
  const untrusted = input;
149
149
  if (typeof untrusted !== "object" || untrusted === null) {
150
- throw new SerializeError(`search input must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
150
+ throw new SerializeError(`search input must be an object, got ${describeType(untrusted)}`);
151
151
  }
152
152
  const pairs = [];
153
153
  const values = untrusted;
@@ -187,6 +187,15 @@ export function encodeSearch(config, input) {
187
187
  }
188
188
  return pairs;
189
189
  }
190
+ /**
191
+ * Runtime discriminant for the `search:` slot (design-04 SS2): the reserved
192
+ * `~kind` marker is unambiguous against a codec map, which never carries a
193
+ * top-level `~`-prefixed key. Module-exported for standard-schema.ts, not
194
+ * barrel-exported.
195
+ */
196
+ export function isRawSearch(config) {
197
+ return "~kind" in config && config["~kind"] === "raw-search";
198
+ }
190
199
  /**
191
200
  * The whole-object search escape hatch (design-04 SS1, maintainer ruling):
192
201
  * an explicit, greppable wrapper around a bare Standard Schema so a route's
@@ -228,6 +237,20 @@ export function readInputValue(values, key) {
228
237
  }
229
238
  return undefined;
230
239
  }
240
+ /**
241
+ * The TS contract makes a non-object config unrepresentable, but a
242
+ * hand-built route missing `~search` reaches both codecs' entry points via
243
+ * href/parseSearch in plain JS; fail branded — a missing config is a
244
+ * config-contract violation (requireCodec's precedent), never a raw
245
+ * TypeError out of Object.entries/Object.keys. Module-exported for
246
+ * standard-schema.ts, not barrel-exported.
247
+ */
248
+ export function requireSearchConfig(config) {
249
+ const untrusted = config;
250
+ if (typeof untrusted !== "object" || untrusted === null) {
251
+ throw new ParamourError(`search config must be an object, got ${describeType(untrusted)}`);
252
+ }
253
+ }
231
254
  /** Convenience: encode + build in one step. */
232
255
  export function searchToString(config, input) {
233
256
  return buildSearchString(encodeSearch(config, input));
@@ -276,7 +299,7 @@ function decodeRawSearch(config, source) {
276
299
  function encodeRawSearch(input) {
277
300
  const untrusted = input;
278
301
  if (typeof untrusted !== "object" || untrusted === null) {
279
- throw new SerializeError(`search input must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
302
+ throw new SerializeError(`search input must be an object, got ${describeType(untrusted)}`);
280
303
  }
281
304
  const values = untrusted;
282
305
  const pairs = [];
@@ -294,14 +317,6 @@ function encodeRawSearch(input) {
294
317
  }
295
318
  return pairs;
296
319
  }
297
- /**
298
- * Runtime discriminant for the `search:` slot (design-04 SS2): the reserved
299
- * `~kind` marker is unambiguous against a codec map, which never carries a
300
- * top-level `~`-prefixed key.
301
- */
302
- function isRawSearch(config) {
303
- return "~kind" in config && config["~kind"] === "raw-search";
304
- }
305
320
  /**
306
321
  * Snapshots EVERY source key's wire values, before any user code (the
307
322
  * whole-object schema's `validate`) runs — sibling of
@@ -317,7 +332,7 @@ function readAllValues(source) {
317
332
  // here; fail branded, not with a raw TypeError out of Object.keys.
318
333
  const untrusted = source;
319
334
  if (typeof untrusted !== "object" || untrusted === null) {
320
- throw new ParamourError(`search source must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
335
+ throw new SearchSourceError(`search source must be an object, got ${describeType(untrusted)}`, null);
321
336
  }
322
337
  const grouped = new Map();
323
338
  if (source instanceof URLSearchParams) {
@@ -326,7 +341,7 @@ function readAllValues(source) {
326
341
  // Record branch below.
327
342
  for (const [key, value] of source) {
328
343
  if (typeof value !== "string") {
329
- throw new ParamourError(`search source values for "${key}" must be strings, got ${typeof value}`);
344
+ throw new SearchSourceError(`search source values for "${key}" must be strings, got ${typeof value}`, key);
330
345
  }
331
346
  const list = grouped.get(key);
332
347
  if (list === undefined)
@@ -363,15 +378,15 @@ function readAllValues(source) {
363
378
  * user code runs. Declared keys only, on purpose: unknown keys are never
364
379
  * validated, so malformed junk under keys paramour doesn't own (qs bracket
365
380
  * params, numbers) can't fail a decode (P8). Malformed values under a
366
- * DECLARED key are a loud {@link ParamourError} — the source doesn't match
367
- * its stated contract — never a silent key drop.
381
+ * DECLARED key are a loud {@link SearchSourceError} — the source doesn't
382
+ * match its stated contract — never a silent key drop.
368
383
  */
369
384
  function readDeclaredValues(config, source) {
370
385
  // The TS contract forbids non-object sources, but plain-JS callers reach
371
386
  // here; fail branded, not with a raw TypeError out of Object.hasOwn.
372
387
  const untrusted = source;
373
388
  if (typeof untrusted !== "object" || untrusted === null) {
374
- throw new ParamourError(`search source must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
389
+ throw new SearchSourceError(`search source must be an object, got ${describeType(untrusted)}`, null);
375
390
  }
376
391
  const values = new Map();
377
392
  if (source instanceof URLSearchParams) {
@@ -383,7 +398,7 @@ function readDeclaredValues(config, source) {
383
398
  if (!Object.hasOwn(config, key))
384
399
  continue;
385
400
  if (typeof value !== "string") {
386
- throw new ParamourError(`search source values for "${key}" must be strings, got ${typeof value}`);
401
+ throw new SearchSourceError(`search source values for "${key}" must be strings, got ${typeof value}`, key);
387
402
  }
388
403
  const list = values.get(key);
389
404
  if (list === undefined)
@@ -417,12 +432,12 @@ function readRecordValues(source, key) {
417
432
  const copy = [...value];
418
433
  for (const element of copy) {
419
434
  if (typeof element !== "string") {
420
- throw new ParamourError(`search source values for "${key}" must be strings, got ${typeof element}`);
435
+ throw new SearchSourceError(`search source values for "${key}" must be strings, got ${typeof element}`, key);
421
436
  }
422
437
  }
423
438
  return copy;
424
439
  }
425
- throw new ParamourError(`search source value for "${key}" must be a string or string[], got ${typeof value}`);
440
+ throw new SearchSourceError(`search source value for "${key}" must be a string or string[], got ${typeof value}`, key);
426
441
  }
427
442
  /**
428
443
  * Property reads run user getters (the class-instance shape
@@ -444,19 +459,6 @@ function requireRawSearchString(key, value) {
444
459
  }
445
460
  return value;
446
461
  }
447
- /**
448
- * The TS contract makes a non-object config unrepresentable, but a
449
- * hand-built route missing `~search` reaches both codecs' entry points via
450
- * href/parseSearch in plain JS; fail branded — a missing config is a
451
- * config-contract violation (requireCodec's precedent), never a raw
452
- * TypeError out of Object.entries/Object.keys.
453
- */
454
- function requireSearchConfig(config) {
455
- const untrusted = config;
456
- if (typeof untrusted !== "object" || untrusted === null) {
457
- throw new ParamourError(`search config must be an object, got ${untrusted === null ? "null" : typeof untrusted}`);
458
- }
459
- }
460
462
  /**
461
463
  * Invokes a codec's serializer and enforces its string contract: a custom
462
464
  * codec written in plain JS can return undefined, which would otherwise
@@ -0,0 +1,27 @@
1
+ import type { StandardSchemaV1 } from "@standard-schema/spec";
2
+ import type { AnyRoute } from "./route.js";
3
+ import { type SearchOutputOf } from "./search.js";
4
+ /**
5
+ * Standard Schema generate-OUT (design-08): exports a route's `search:`
6
+ * config as a spec-compliant Standard Schema. The mirror of schema.ts, which
7
+ * runs Standard Schemas coming IN.
8
+ */
9
+ /**
10
+ * The schema {@link standardSearchSchema} returns (STD1/STD3): input is
11
+ * advertised as the wire-shaped record only — `URLSearchParams` is accepted
12
+ * at runtime but kept out of the type, so client-side inference (tRPC) never
13
+ * sees a shape that cannot serialize over JSON. Output is the route's decoded
14
+ * search shape. `types` is carried by this annotation alone; the spec reads
15
+ * it at the type level only, so no runtime key exists.
16
+ */
17
+ export type StandardSearchSchema<SC> = StandardSchemaV1<Record<string, string | string[] | undefined>, SearchOutputOf<SC>>;
18
+ /**
19
+ * Exports a route's `search:` config as the URL wire contract in Standard
20
+ * Schema form (design-08 STD1/STD5), for consumers like tRPC inputs or
21
+ * TanStack `validateSearch`. Semantics are byte-identical to `decodeSearch`
22
+ * (STD6): defaults apply, `.catch()` recovers parse failures — invalid API
23
+ * input silently coerces to the fallback — unknown keys strip (P8), and
24
+ * duplicate values on a scalar codec reject (P5). No coercion, ever (STD2):
25
+ * the schema accepts wire strings (`"42"`), not decoded values (`42`).
26
+ */
27
+ export declare function standardSearchSchema<R extends AnyRoute>(route: R): StandardSearchSchema<R["~search"]>;
@@ -0,0 +1,77 @@
1
+ import { SearchSourceError } from "./errors.js";
2
+ import { safeDecodeSearch } from "./safe-decode.js";
3
+ import { isRawSearch, requireSearchConfig, } from "./search.js";
4
+ /**
5
+ * Exports a route's `search:` config as the URL wire contract in Standard
6
+ * Schema form (design-08 STD1/STD5), for consumers like tRPC inputs or
7
+ * TanStack `validateSearch`. Semantics are byte-identical to `decodeSearch`
8
+ * (STD6): defaults apply, `.catch()` recovers parse failures — invalid API
9
+ * input silently coerces to the fallback — unknown keys strip (P8), and
10
+ * duplicate values on a scalar codec reject (P5). No coercion, ever (STD2):
11
+ * the schema accepts wire strings (`"42"`), not decoded values (`42`).
12
+ */
13
+ export function standardSearchSchema(route) {
14
+ const config = route["~search"];
15
+ // A missing/malformed config is a programming error and stays loud (STD7);
16
+ // checking eagerly fails at construction, not at first validate().
17
+ requireSearchConfig(config);
18
+ // A config's raw/codec-map shape is fixed at construction; hoisted so the
19
+ // sentinel un-mapping below never touches a codec map's keyed issues.
20
+ const raw = isRawSearch(config);
21
+ return {
22
+ "~standard": {
23
+ validate: (value) => {
24
+ try {
25
+ const result = safeDecodeSearch(route, value);
26
+ if (result.status === "error") {
27
+ return {
28
+ issues: result.error.issues.map((issue) => toStandardIssue(issue, raw)),
29
+ };
30
+ }
31
+ return { value: result.data };
32
+ }
33
+ catch (error) {
34
+ // STD7: validate() receives genuinely untrusted input, so the
35
+ // source-shape contract the read layer enforces with loud throws
36
+ // softens to issues at this one boundary — SearchSourceError exists
37
+ // as its own branded class exactly so this catch can't swallow
38
+ // config-contract violations or rebranded validator throws.
39
+ if (error instanceof SearchSourceError) {
40
+ return { issues: [toSourceIssue(error)] };
41
+ }
42
+ // Async raw schema (design-02 D7), throwing raw validators, and
43
+ // throwing .default()/.catch() factories are true programming
44
+ // errors: loud (STD7).
45
+ throw error;
46
+ }
47
+ },
48
+ vendor: "paramour",
49
+ version: 1,
50
+ },
51
+ };
52
+ }
53
+ /**
54
+ * Maps a source-shape violation to spec shape (STD7): keyed where the read
55
+ * layer could attribute it, root-level (absent path) for a malformed source.
56
+ */
57
+ function toSourceIssue(error) {
58
+ return error.key === null
59
+ ? { message: error.message }
60
+ : { message: error.message, path: [error.key] };
61
+ }
62
+ /**
63
+ * Maps a decode issue to spec shape. decodeRawSearch collapses a root-level
64
+ * schema issue to the "<search>" sentinel key (SS3/SS4); a Standard Schema
65
+ * expresses "root" as an ABSENT path, so the sentinel un-maps here (STD7
66
+ * "root-level otherwise") — for raw configs only, since a codec map's issues
67
+ * are always keyed and a param may literally be named "<search>". (A raw
68
+ * vendor issue whose real path is ["<search>"] still collides with the
69
+ * sentinel — indistinguishable after the flat-key collapse.) Nested
70
+ * raw-schema paths were already dot-joined into the key and re-emit as ONE
71
+ * segment — keys may contain dots, so splitting back would be unsound.
72
+ */
73
+ function toStandardIssue(issue, raw) {
74
+ return raw && issue.key === "<search>"
75
+ ? { message: issue.message }
76
+ : { message: issue.message, path: [issue.key] };
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "paramour",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -33,6 +33,10 @@
33
33
  "files": [
34
34
  "dist"
35
35
  ],
36
+ "publishConfig": {
37
+ "access": "public",
38
+ "provenance": true
39
+ },
36
40
  "dependencies": {
37
41
  "@standard-schema/spec": "^1.1.0"
38
42
  },