@rudderjs/router 1.2.0 → 1.3.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,79 @@
1
+ import type { HttpMethod, MiddlewareHandler } from '@rudderjs/contracts';
2
+ import type { Router, RouteBuilder } from './index.js';
3
+ /** The seven canonical RESTful verbs Laravel's `Route::resource` exposes. */
4
+ export type ResourceVerb = 'index' | 'create' | 'store' | 'show' | 'edit' | 'update' | 'destroy';
5
+ /** @internal — shape of a single row in the verb tables below. */
6
+ export interface ResourceVerbSpec {
7
+ verb: ResourceVerb;
8
+ method: HttpMethod;
9
+ path: (name: string, param: string) => string;
10
+ nameSuffix: string;
11
+ }
12
+ /** @internal — verb table for `resource()` / `apiResource()`. */
13
+ export declare const RESOURCE_VERBS: readonly ResourceVerbSpec[];
14
+ /** @internal — verb table for `singleton()`. */
15
+ export declare const SINGLETON_VERBS: readonly ResourceVerbSpec[];
16
+ /** @internal — `.creatable()` opt-in for singletons. */
17
+ export declare const SINGLETON_CREATE_VERBS: readonly ResourceVerbSpec[];
18
+ /** @internal — `.destroyable()` opt-in for singletons. */
19
+ export declare const SINGLETON_DESTROY_VERBS: readonly ResourceVerbSpec[];
20
+ /** @internal — apply `only` / `except` to a verb table. */
21
+ export declare function filterVerbs(table: readonly ResourceVerbSpec[], opts: ResourceOptions): readonly ResourceVerbSpec[];
22
+ /**
23
+ * @internal — naive English singularizer for the default resource param name.
24
+ * Handles the three patterns Laravel users hit constantly (`posts → post`,
25
+ * `categories → category`, `boxes → box`). Anything irregular — `people`,
26
+ * `data`, etc. — should be overridden via the `parameters` option, exactly
27
+ * as in Laravel.
28
+ */
29
+ export declare function singularize(name: string): string;
30
+ /**
31
+ * Options accepted by `router.resource`/`apiResource`/`singleton`.
32
+ *
33
+ * - `only`/`except` — restrict the verbs registered.
34
+ * - `parameters` — override the `:param` segment name for a given resource
35
+ * (e.g. `{ posts: 'article' }` → `/posts/:article`).
36
+ * - `names` — override the generated route names per verb.
37
+ * - `middleware` — applied to every route registered by the resource.
38
+ */
39
+ export interface ResourceOptions {
40
+ only?: readonly ResourceVerb[];
41
+ except?: readonly ResourceVerb[];
42
+ parameters?: Record<string, string>;
43
+ names?: Partial<Record<ResourceVerb, string>>;
44
+ middleware?: MiddlewareHandler[];
45
+ }
46
+ /**
47
+ * Returned by `router.resource()`/`apiResource()`. The `builders` array holds
48
+ * one `RouteBuilder` per registered route in declaration order — apply
49
+ * `where*()`, additional middleware, or rename individual routes by indexing
50
+ * directly. The `update` PATCH alias is included as a separate builder
51
+ * immediately after its PUT counterpart.
52
+ */
53
+ export declare class ResourceRegistration {
54
+ readonly builders: RouteBuilder[];
55
+ constructor(builders: RouteBuilder[]);
56
+ }
57
+ /**
58
+ * Returned by `router.singleton()`. Adds two opt-in helpers on top of
59
+ * `ResourceRegistration` for resources that also expose a creation flow
60
+ * (`.creatable()`) or deletion flow (`.destroyable()`).
61
+ */
62
+ export declare class SingletonRegistration extends ResourceRegistration {
63
+ private readonly _router;
64
+ private readonly _name;
65
+ private readonly _Ctrl;
66
+ private readonly _opts;
67
+ constructor(builders: RouteBuilder[], _router: Router, _name: string, _Ctrl: new () => object, _opts: ResourceOptions);
68
+ /**
69
+ * Add `GET /<name>/create` and `POST /<name>` — the create/store half of a
70
+ * full resource. Skipped for any verb the controller doesn't implement.
71
+ */
72
+ creatable(): this;
73
+ /**
74
+ * Add `DELETE /<name>` — the destroy half of a full resource. Skipped if
75
+ * the controller doesn't implement `destroy()`.
76
+ */
77
+ destroyable(): this;
78
+ }
79
+ //# sourceMappingURL=resource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AACxE,OAAO,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAItD,6EAA6E;AAC7E,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAA;AAEhG,kEAAkE;AAClE,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAQ,YAAY,CAAA;IACxB,MAAM,EAAM,UAAU,CAAA;IACtB,IAAI,EAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAA;IACnD,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,iEAAiE;AACjE,eAAO,MAAM,cAAc,EAAE,SAAS,gBAAgB,EAQrD,CAAA;AAED,gDAAgD;AAChD,eAAO,MAAM,eAAe,EAAE,SAAS,gBAAgB,EAItD,CAAA;AAED,wDAAwD;AACxD,eAAO,MAAM,sBAAsB,EAAE,SAAS,gBAAgB,EAG7D,CAAA;AAED,0DAA0D;AAC1D,eAAO,MAAM,uBAAuB,EAAE,SAAS,gBAAgB,EAE9D,CAAA;AAED,2DAA2D;AAC3D,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,gBAAgB,EAAE,EAAE,IAAI,EAAE,eAAe,GAAG,SAAS,gBAAgB,EAAE,CAKlH;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAKhD;AAID;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAQ,SAAS,YAAY,EAAE,CAAA;IACpC,MAAM,CAAC,EAAM,SAAS,YAAY,EAAE,CAAA;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,KAAK,CAAC,EAAO,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAA;IAClD,UAAU,CAAC,EAAE,iBAAiB,EAAE,CAAA;CACjC;AAED;;;;;;GAMG;AACH,qBAAa,oBAAoB;aACH,QAAQ,EAAE,YAAY,EAAE;gBAAxB,QAAQ,EAAE,YAAY,EAAE;CACrD;AAED;;;;GAIG;AACH,qBAAa,qBAAsB,SAAQ,oBAAoB;IAG3D,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAJtB,QAAQ,EAAE,YAAY,EAAE,EACP,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,UAAU,MAAM,EACvB,KAAK,EAAE,eAAe;IAGzC;;;OAGG;IACH,SAAS,IAAI,IAAI;IAMjB;;;OAGG;IACH,WAAW,IAAI,IAAI;CAKpB"}
@@ -0,0 +1,104 @@
1
+ /** @internal — verb table for `resource()` / `apiResource()`. */
2
+ export const RESOURCE_VERBS = [
3
+ { verb: 'index', method: 'GET', path: (n) => `/${n}`, nameSuffix: 'index' },
4
+ { verb: 'create', method: 'GET', path: (n) => `/${n}/create`, nameSuffix: 'create' },
5
+ { verb: 'store', method: 'POST', path: (n) => `/${n}`, nameSuffix: 'store' },
6
+ { verb: 'show', method: 'GET', path: (n, p) => `/${n}/:${p}`, nameSuffix: 'show' },
7
+ { verb: 'edit', method: 'GET', path: (n, p) => `/${n}/:${p}/edit`, nameSuffix: 'edit' },
8
+ { verb: 'update', method: 'PUT', path: (n, p) => `/${n}/:${p}`, nameSuffix: 'update' },
9
+ { verb: 'destroy', method: 'DELETE', path: (n, p) => `/${n}/:${p}`, nameSuffix: 'destroy' },
10
+ ];
11
+ /** @internal — verb table for `singleton()`. */
12
+ export const SINGLETON_VERBS = [
13
+ { verb: 'show', method: 'GET', path: (n) => `/${n}`, nameSuffix: 'show' },
14
+ { verb: 'edit', method: 'GET', path: (n) => `/${n}/edit`, nameSuffix: 'edit' },
15
+ { verb: 'update', method: 'PUT', path: (n) => `/${n}`, nameSuffix: 'update' },
16
+ ];
17
+ /** @internal — `.creatable()` opt-in for singletons. */
18
+ export const SINGLETON_CREATE_VERBS = [
19
+ { verb: 'create', method: 'GET', path: (n) => `/${n}/create`, nameSuffix: 'create' },
20
+ { verb: 'store', method: 'POST', path: (n) => `/${n}`, nameSuffix: 'store' },
21
+ ];
22
+ /** @internal — `.destroyable()` opt-in for singletons. */
23
+ export const SINGLETON_DESTROY_VERBS = [
24
+ { verb: 'destroy', method: 'DELETE', path: (n) => `/${n}`, nameSuffix: 'destroy' },
25
+ ];
26
+ /** @internal — apply `only` / `except` to a verb table. */
27
+ export function filterVerbs(table, opts) {
28
+ let verbs = table;
29
+ if (opts.only) {
30
+ const allow = new Set(opts.only);
31
+ verbs = verbs.filter(v => allow.has(v.verb));
32
+ }
33
+ if (opts.except) {
34
+ const deny = new Set(opts.except);
35
+ verbs = verbs.filter(v => !deny.has(v.verb));
36
+ }
37
+ return verbs;
38
+ }
39
+ /**
40
+ * @internal — naive English singularizer for the default resource param name.
41
+ * Handles the three patterns Laravel users hit constantly (`posts → post`,
42
+ * `categories → category`, `boxes → box`). Anything irregular — `people`,
43
+ * `data`, etc. — should be overridden via the `parameters` option, exactly
44
+ * as in Laravel.
45
+ */
46
+ export function singularize(name) {
47
+ if (/[^aeiou]ies$/i.test(name))
48
+ return name.slice(0, -3) + 'y'; // categories → category
49
+ if (/(s|x|z|ch|sh)es$/i.test(name))
50
+ return name.slice(0, -2); // boxes → box
51
+ if (/s$/i.test(name) && !/ss$/i.test(name))
52
+ return name.slice(0, -1); // posts → post
53
+ return name;
54
+ }
55
+ /**
56
+ * Returned by `router.resource()`/`apiResource()`. The `builders` array holds
57
+ * one `RouteBuilder` per registered route in declaration order — apply
58
+ * `where*()`, additional middleware, or rename individual routes by indexing
59
+ * directly. The `update` PATCH alias is included as a separate builder
60
+ * immediately after its PUT counterpart.
61
+ */
62
+ export class ResourceRegistration {
63
+ builders;
64
+ constructor(builders) {
65
+ this.builders = builders;
66
+ }
67
+ }
68
+ /**
69
+ * Returned by `router.singleton()`. Adds two opt-in helpers on top of
70
+ * `ResourceRegistration` for resources that also expose a creation flow
71
+ * (`.creatable()`) or deletion flow (`.destroyable()`).
72
+ */
73
+ export class SingletonRegistration extends ResourceRegistration {
74
+ _router;
75
+ _name;
76
+ _Ctrl;
77
+ _opts;
78
+ constructor(builders, _router, _name, _Ctrl, _opts) {
79
+ super(builders);
80
+ this._router = _router;
81
+ this._name = _name;
82
+ this._Ctrl = _Ctrl;
83
+ this._opts = _opts;
84
+ }
85
+ /**
86
+ * Add `GET /<name>/create` and `POST /<name>` — the create/store half of a
87
+ * full resource. Skipped for any verb the controller doesn't implement.
88
+ */
89
+ creatable() {
90
+ const reg = this._router._registerResource(this._name, this._Ctrl, SINGLETON_CREATE_VERBS, this._opts);
91
+ this.builders.push(...reg.builders);
92
+ return this;
93
+ }
94
+ /**
95
+ * Add `DELETE /<name>` — the destroy half of a full resource. Skipped if
96
+ * the controller doesn't implement `destroy()`.
97
+ */
98
+ destroyable() {
99
+ const reg = this._router._registerResource(this._name, this._Ctrl, SINGLETON_DESTROY_VERBS, this._opts);
100
+ this.builders.push(...reg.builders);
101
+ return this;
102
+ }
103
+ }
104
+ //# sourceMappingURL=resource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resource.js","sourceRoot":"","sources":["../src/resource.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import type { AppRequest, AppResponse } from '@rudderjs/contracts';
2
+ /**
3
+ * Parse a single `:name` (or `:name?`, or `:name{regex}`, or `:name?{regex}`)
4
+ * out of the head of a string. Returns `{ name, optional, rest }` or `never`
5
+ * if no name letter follows the `:`.
6
+ */
7
+ type ParseParam<S extends string> = S extends `${infer Head}${infer Tail}` ? Head extends Letter ? AccumName<Tail, Head> : never : never;
8
+ type Letter = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '_';
9
+ type AccumName<S extends string, Acc extends string> = S extends `${infer Head}${infer Tail}` ? Head extends Letter ? AccumName<Tail, `${Acc}${Head}`> : FinishName<S, Acc> : FinishName<S, Acc>;
10
+ type FinishName<S extends string, Name extends string> = S extends `?${infer Rest}` ? {
11
+ name: Name;
12
+ optional: true;
13
+ rest: StripBraceBlock<Rest>;
14
+ } : {
15
+ name: Name;
16
+ optional: false;
17
+ rest: StripBraceBlock<S>;
18
+ };
19
+ /** Skip a `{...}` regex constraint block (single level; mirrors router's parser). */
20
+ type StripBraceBlock<S extends string> = S extends `{${infer _Body}}${infer Rest}` ? Rest : S;
21
+ /**
22
+ * Walk the path and accumulate every `:param` into a union of
23
+ * `{ name; optional }` records.
24
+ */
25
+ type ScanParams<P extends string, Acc = never> = P extends `${infer _Head}:${infer Rest}` ? ParseParam<Rest> extends {
26
+ name: infer N;
27
+ optional: infer O;
28
+ rest: infer R;
29
+ } ? N extends string ? R extends string ? ScanParams<R, Acc | {
30
+ name: N;
31
+ optional: O;
32
+ }> : Acc : Acc : ScanParams<Rest, Acc> : Acc;
33
+ /**
34
+ * Map the param union to an object. Optional params become optional keys.
35
+ *
36
+ * Tricks used here:
37
+ * - `as` re-keys the mapped type so the property name is the captured `name`.
38
+ * - Splitting required vs optional via intersection lets us mark optional
39
+ * keys with `?:` while keeping required ones as `:`. TS doesn't allow
40
+ * conditional optionality inside a single mapped type.
41
+ */
42
+ type ParamUnion<P extends string> = ScanParams<P>;
43
+ type RequiredParams<P extends string> = {
44
+ [U in Extract<ParamUnion<P>, {
45
+ optional: false;
46
+ }> as U['name']]: string;
47
+ };
48
+ type OptionalParams<P extends string> = {
49
+ [U in Extract<ParamUnion<P>, {
50
+ optional: true;
51
+ }> as U['name']]?: string;
52
+ };
53
+ /**
54
+ * Object shape of `req.params` for a literal route path.
55
+ *
56
+ * @example
57
+ * type T1 = ExtractParams<'/users/:id'> // { id: string }
58
+ * type T2 = ExtractParams<'/users/:id/posts/:postId'> // { id: string; postId: string }
59
+ * type T3 = ExtractParams<'/files/:name?'> // { name?: string }
60
+ * type T4 = ExtractParams<'/health'> // {}
61
+ */
62
+ export type ExtractParams<P extends string> = Prettify<RequiredParams<P> & OptionalParams<P>>;
63
+ /** Flatten an intersection into a single object literal for nicer hovers. */
64
+ type Prettify<T> = {
65
+ [K in keyof T]: T[K];
66
+ } & {};
67
+ /**
68
+ * `AppRequest` with `params` and `query` overridden to the inferred shapes.
69
+ * All other fields (including module-augmented `user`, `session`, `token`)
70
+ * are inherited via `Omit + extend`.
71
+ */
72
+ export interface TypedRequest<P extends Record<string, string | undefined> = Record<string, string>, Q = Record<string, string>> extends Omit<AppRequest, 'params' | 'query'> {
73
+ params: P;
74
+ query: Q;
75
+ }
76
+ /**
77
+ * Handler whose `req.params` is derived from the literal path `P`, and whose
78
+ * `req.query` is `Q` (defaulted to `Record<string, string>`; replaced via
79
+ * `{ query: schema }` opts in the route declaration).
80
+ */
81
+ export type TypedHandler<P extends string, Q = Record<string, string>> = (req: TypedRequest<ExtractParams<P>, Q>, res: AppResponse) => unknown | Promise<unknown>;
82
+ export {};
83
+ //# sourceMappingURL=typed-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed-routes.d.ts","sourceRoot":"","sources":["../src/typed-routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA;AAiBlE;;;;GAIG;AACH,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,IAC9B,CAAC,SAAS,GAAG,MAAM,IAAI,GAAG,MAAM,IAAI,EAAE,GAClC,IAAI,SAAS,MAAM,GACjB,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC,GACrB,KAAK,GACP,KAAK,CAAA;AAEX,KAAK,MAAM,GACP,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GACzD,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GACzD,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GACjC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GACzD,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GACzD,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GACjC,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GACzD,GAAG,CAAA;AAEP,KAAK,SAAS,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,IACjD,CAAC,SAAS,GAAG,MAAM,IAAI,GAAG,MAAM,IAAI,EAAE,GAClC,IAAI,SAAS,MAAM,GACjB,SAAS,CAAC,IAAI,EAAE,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC,GAChC,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,GACpB,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AAExB,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,IAAI,SAAS,MAAM,IACnD,CAAC,SAAS,IAAI,MAAM,IAAI,EAAE,GACtB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAC;IAAE,IAAI,EAAE,eAAe,CAAC,IAAI,CAAC,CAAA;CAAE,GAC5D;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,KAAK,CAAC;IAAC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,CAAA;CAAE,CAAA;AAE/D,qFAAqF;AACrF,KAAK,eAAe,CAAC,CAAC,SAAS,MAAM,IACnC,CAAC,SAAS,IAAI,MAAM,KAAK,IAAI,MAAM,IAAI,EAAE,GAAG,IAAI,GAAG,CAAC,CAAA;AAEtD;;;GAGG;AACH,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,GAAG,KAAK,IAC3C,CAAC,SAAS,GAAG,MAAM,KAAK,IAAI,MAAM,IAAI,EAAE,GACpC,UAAU,CAAC,IAAI,CAAC,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC,CAAA;CAAE,GAC1E,CAAC,SAAS,MAAM,GACd,CAAC,SAAS,MAAM,GACd,UAAU,CAAC,CAAC,EAAE,GAAG,GAAG;IAAE,IAAI,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,CAAC,CAAA;CAAE,CAAC,GAC7C,GAAG,GACL,GAAG,GACL,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,GACvB,GAAG,CAAA;AAET;;;;;;;;GAQG;AACH,KAAK,UAAU,CAAC,CAAC,SAAS,MAAM,IAAI,UAAU,CAAC,CAAC,CAAC,CAAA;AAEjD,KAAK,cAAc,CAAC,CAAC,SAAS,MAAM,IAAI;KACrC,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;QAAE,QAAQ,EAAE,KAAK,CAAA;KAAE,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,GAAG,MAAM;CACxE,CAAA;AAED,KAAK,cAAc,CAAC,CAAC,SAAS,MAAM,IAAI;KACrC,CAAC,IAAI,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;QAAE,QAAQ,EAAE,IAAI,CAAA;KAAE,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM;CACxE,CAAA;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,IACxC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAA;AAEjD,6EAA6E;AAC7E,KAAK,QAAQ,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG,EAAE,CAAA;AAIhD;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACrE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAC1B,SAAQ,IAAI,CAAC,UAAU,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC5C,MAAM,EAAE,CAAC,CAAA;IACT,KAAK,EAAG,CAAC,CAAA;CACV;AAED;;;;GAIG;AACH,MAAM,MAAM,YAAY,CACtB,CAAC,SAAS,MAAM,EAChB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IACxB,CACF,GAAG,EAAE,YAAY,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EACtC,GAAG,EAAE,WAAW,KACb,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=typed-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed-routes.js","sourceRoot":"","sources":["../src/typed-routes.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=typed-routes.test-d.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed-routes.test-d.d.ts","sourceRoot":"","sources":["../src/typed-routes.test-d.ts"],"names":[],"mappings":"AAgHA,OAAO,EAAE,CAAA"}
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Type-only test for `ExtractParams<P>` + typed handler signatures.
3
+ *
4
+ * Compiled by `pnpm typecheck` (and the test build); failing type assertions
5
+ * surface as tsc errors. No runtime tests live here — Node's `--test` glob
6
+ * excludes `*.test-d.js`.
7
+ */
8
+ import { z } from 'zod';
9
+ import { Router } from './index.js';
10
+ const router = new Router();
11
+ const check = () => undefined;
12
+ check();
13
+ check();
14
+ check();
15
+ check();
16
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
17
+ check();
18
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
19
+ check();
20
+ // ─── Handler typing (positive) ─────────────────────────────
21
+ router.get('/users/:id', (req) => {
22
+ const id = req.params.id;
23
+ return id;
24
+ });
25
+ router.post('/users/:id/posts/:postId', (req) => {
26
+ const id = req.params.id;
27
+ const postId = req.params.postId;
28
+ return { id, postId };
29
+ });
30
+ router.get('/files/:name?', (req) => {
31
+ const name = req.params.name;
32
+ return name;
33
+ });
34
+ router.get('/health', (req) => {
35
+ // No params — `req.params` is `{}`. Reading any key would be an error.
36
+ return req.params;
37
+ });
38
+ // ─── Handler typing (negative) ─────────────────────────────
39
+ router.get('/users/:id', (req) => {
40
+ // @ts-expect-error `notReal` is not a declared param
41
+ return req.params.notReal;
42
+ });
43
+ router.get('/users/:id', (req) => {
44
+ // @ts-expect-error optional access pattern on required param — `id` is `string`, not nullable
45
+ const id = req.params.id;
46
+ return id;
47
+ });
48
+ router.get('/files/:name?', (req) => {
49
+ // @ts-expect-error `name` is `string | undefined`, not assignable to `string`
50
+ const name = req.params.name;
51
+ return name;
52
+ });
53
+ // ─── Opts form: req.query inferred from Zod schema ─────────
54
+ router.get('/users/:id', { query: z.object({ page: z.coerce.number(), q: z.string() }) }, (req) => {
55
+ const id = req.params.id;
56
+ const page = req.query.page;
57
+ const q = req.query.q;
58
+ return { id, page, q };
59
+ });
60
+ router.get('/search', { query: z.object({ q: z.string() }) }, (req) => {
61
+ // @ts-expect-error `page` is not in the query schema
62
+ return req.query.page;
63
+ });
64
+ router.get('/search', { query: z.object({ q: z.string() }) }, (req) => {
65
+ // @ts-expect-error `q` is `string`, not `number`
66
+ const q = req.query.q;
67
+ return q;
68
+ });
69
+ // ─── Other AppRequest fields still accessible ──────────────
70
+ router.get('/health', (req) => {
71
+ const method = req.method;
72
+ const url = req.url;
73
+ const headers = req.headers;
74
+ const query = req.query;
75
+ return { method, url, headers, query };
76
+ });
77
+ //# sourceMappingURL=typed-routes.test-d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typed-routes.test-d.js","sourceRoot":"","sources":["../src/typed-routes.test-d.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import type { AppRequest, MiddlewareHandler } from '@rudderjs/contracts';
2
+ export declare class Url {
3
+ /**
4
+ * Override the HMAC signing key used for signed URLs.
5
+ * Falls back to `process.env.APP_KEY`.
6
+ */
7
+ static setKey(key: string): void;
8
+ /** The full URL of the current request. */
9
+ static current(req: AppRequest): string;
10
+ /** The previous URL from the `Referer` header, or `fallback`. */
11
+ static previous(req: AppRequest, fallback?: string): string;
12
+ /**
13
+ * Generate a signed URL for a named route.
14
+ *
15
+ * @example
16
+ * Url.signedRoute('invoice.download', { id: 42 })
17
+ * // → '/invoice/42?signature=abc123'
18
+ */
19
+ static signedRoute(name: string, params?: Record<string, string | number>, expiresAt?: Date): string;
20
+ /**
21
+ * Generate a signed URL that expires after `seconds` seconds.
22
+ *
23
+ * @example
24
+ * Url.temporarySignedRoute('invoice.download', 3600, { id: 42 })
25
+ * // → '/invoice/42?expires=1234567890&signature=abc123'
26
+ */
27
+ static temporarySignedRoute(name: string, seconds: number, params?: Record<string, string | number>): string;
28
+ /**
29
+ * Sign an arbitrary path string.
30
+ * Appends `?signature=...` (and `?expires=...` if `expiresAt` given).
31
+ */
32
+ static sign(path: string, expiresAt?: Date): string;
33
+ /**
34
+ * Return `true` if the request has a valid (and non-expired) signature.
35
+ */
36
+ static isValidSignature(req: AppRequest): boolean;
37
+ }
38
+ /**
39
+ * Middleware that verifies a signed URL signature.
40
+ * Responds with 403 if the signature is missing, invalid, or expired.
41
+ *
42
+ * @example
43
+ * router.get('/invoice/:id/download', handler, [ValidateSignature()])
44
+ */
45
+ export declare function ValidateSignature(): MiddlewareHandler;
46
+ //# sourceMappingURL=url-signing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-signing.d.ts","sourceRoot":"","sources":["../src/url-signing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AA6CxE,qBAAa,GAAG;IACd;;;OAGG;IACH,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIhC,2CAA2C;IAC3C,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,MAAM;IAIvC,iEAAiE;IACjE,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,UAAU,EAAE,QAAQ,SAAM,GAAG,MAAM;IAIxD;;;;;;OAMG;IACH,MAAM,CAAC,WAAW,CAChB,IAAI,EAAE,MAAM,EACZ,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAM,EAC5C,SAAS,CAAC,EAAE,IAAI,GACf,MAAM;IAIT;;;;;;OAMG;IACH,MAAM,CAAC,oBAAoB,CACzB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAM,GAC3C,MAAM;IAIT;;;OAGG;IACH,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,IAAI,GAAG,MAAM;IAcnD;;OAEG;IACH,MAAM,CAAC,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO;CA2BlD;AAID;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,iBAAiB,CAOrD"}
@@ -0,0 +1,133 @@
1
+ import { route } from './index.js';
2
+ // ─── node:crypto lazy load ─────────────────────────────────
3
+ //
4
+ // Lazy-load node:crypto to avoid bundling it into the client. Only used by
5
+ // Url (signed URLs) and ValidateSignature — server-only features. The
6
+ // fire-and-forget import preloads on server and is a no-op in the browser
7
+ // where `globalThis.process` is undefined.
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ let _crypto;
10
+ if (typeof globalThis.process !== 'undefined') {
11
+ import(/* @vite-ignore */ 'node:crypto').then(m => { _crypto = m; }).catch(() => { });
12
+ }
13
+ // ─── Signing key + helpers ─────────────────────────────────
14
+ let _urlKey = '';
15
+ function _getSigningKey() {
16
+ const key = _urlKey || process.env['APP_KEY'] || '';
17
+ if (!key)
18
+ throw new Error('[RudderJS] No signing key configured. Set APP_KEY in your .env or call Url.setKey().');
19
+ return key;
20
+ }
21
+ function _splitPath(path) {
22
+ const idx = path.indexOf('?');
23
+ return idx === -1 ? [path, ''] : [path.slice(0, idx), path.slice(idx + 1)];
24
+ }
25
+ function _computeSignature(pathname, params) {
26
+ // Sort params for deterministic signing (exclude 'signature' itself)
27
+ const sorted = new URLSearchParams([...params.entries()]
28
+ .filter(([k]) => k !== 'signature')
29
+ .sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
30
+ const toSign = sorted.size > 0 ? `${pathname}?${sorted.toString()}` : pathname;
31
+ if (!_crypto)
32
+ throw new Error('[RudderJS Router] node:crypto not available — Url signing requires a server environment.');
33
+ return _crypto.createHmac('sha256', _getSigningKey()).update(toSign).digest('hex');
34
+ }
35
+ // ─── Url ───────────────────────────────────────────────────
36
+ export class Url {
37
+ /**
38
+ * Override the HMAC signing key used for signed URLs.
39
+ * Falls back to `process.env.APP_KEY`.
40
+ */
41
+ static setKey(key) {
42
+ _urlKey = key;
43
+ }
44
+ /** The full URL of the current request. */
45
+ static current(req) {
46
+ return req.url;
47
+ }
48
+ /** The previous URL from the `Referer` header, or `fallback`. */
49
+ static previous(req, fallback = '/') {
50
+ return req.headers['referer'] ?? fallback;
51
+ }
52
+ /**
53
+ * Generate a signed URL for a named route.
54
+ *
55
+ * @example
56
+ * Url.signedRoute('invoice.download', { id: 42 })
57
+ * // → '/invoice/42?signature=abc123'
58
+ */
59
+ static signedRoute(name, params = {}, expiresAt) {
60
+ return Url.sign(route(name, params), expiresAt);
61
+ }
62
+ /**
63
+ * Generate a signed URL that expires after `seconds` seconds.
64
+ *
65
+ * @example
66
+ * Url.temporarySignedRoute('invoice.download', 3600, { id: 42 })
67
+ * // → '/invoice/42?expires=1234567890&signature=abc123'
68
+ */
69
+ static temporarySignedRoute(name, seconds, params = {}) {
70
+ return Url.signedRoute(name, params, new Date(Date.now() + seconds * 1000));
71
+ }
72
+ /**
73
+ * Sign an arbitrary path string.
74
+ * Appends `?signature=...` (and `?expires=...` if `expiresAt` given).
75
+ */
76
+ static sign(path, expiresAt) {
77
+ const [pathname, search] = _splitPath(path);
78
+ const params = new URLSearchParams(search);
79
+ if (expiresAt) {
80
+ params.set('expires', String(Math.floor(expiresAt.getTime() / 1000)));
81
+ }
82
+ const sig = _computeSignature(pathname, params);
83
+ params.set('signature', sig);
84
+ return `${pathname}?${params.toString()}`;
85
+ }
86
+ /**
87
+ * Return `true` if the request has a valid (and non-expired) signature.
88
+ */
89
+ static isValidSignature(req) {
90
+ // `req.url` may be a full URL (Hono adapter populates protocol+host+path+query)
91
+ // or a bare path. `Url.sign(path)` only ever signs the pathname, so verification
92
+ // must hash the same shape. Use the URL parser so both forms collapse to a
93
+ // pathname + searchParams pair.
94
+ const u = new URL(req.url, 'http://placeholder.local');
95
+ const pathname = u.pathname;
96
+ const params = u.searchParams;
97
+ const signature = params.get('signature');
98
+ if (!signature)
99
+ return false;
100
+ // Check expiry before touching the signature
101
+ const expires = params.get('expires');
102
+ if (expires !== null) {
103
+ const expiry = parseInt(expires, 10);
104
+ if (isNaN(expiry) || Date.now() / 1000 > expiry)
105
+ return false;
106
+ }
107
+ const expected = _computeSignature(pathname, params);
108
+ if (!_crypto)
109
+ return signature === expected;
110
+ const sigBuf = Buffer.from(signature);
111
+ const expBuf = Buffer.from(expected);
112
+ if (sigBuf.length !== expBuf.length)
113
+ return false;
114
+ return _crypto.timingSafeEqual(sigBuf, expBuf);
115
+ }
116
+ }
117
+ // ─── ValidateSignature middleware ───────────────────────────
118
+ /**
119
+ * Middleware that verifies a signed URL signature.
120
+ * Responds with 403 if the signature is missing, invalid, or expired.
121
+ *
122
+ * @example
123
+ * router.get('/invoice/:id/download', handler, [ValidateSignature()])
124
+ */
125
+ export function ValidateSignature() {
126
+ return async (req, res, next) => {
127
+ if (!Url.isValidSignature(req)) {
128
+ return res.status(403).json({ message: 'Invalid or expired URL signature.' });
129
+ }
130
+ await next();
131
+ };
132
+ }
133
+ //# sourceMappingURL=url-signing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"url-signing.js","sourceRoot":"","sources":["../src/url-signing.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rudderjs/router",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,6 +8,9 @@
8
8
  "directory": "packages/router"
9
9
  },
10
10
  "type": "module",
11
+ "engines": {
12
+ "node": "^20.19.0 || >=22.12.0"
13
+ },
11
14
  "files": [
12
15
  "dist",
13
16
  "boost"
@@ -27,7 +30,8 @@
27
30
  },
28
31
  "dependencies": {
29
32
  "reflect-metadata": "^0.2.2",
30
- "@rudderjs/contracts": "^1.4.0"
33
+ "zod": "^4.0.0",
34
+ "@rudderjs/contracts": "^1.7.0"
31
35
  },
32
36
  "devDependencies": {
33
37
  "@types/node": "^20.0.0",
@@ -42,6 +46,6 @@
42
46
  "typecheck": "tsc --noEmit",
43
47
  "lint": "eslint src",
44
48
  "clean": "rm -rf dist",
45
- "test": "tsc -p tsconfig.test.json && node --test dist-test/index.test.js dist-test/resource.test.js; EXIT=$?; rm -rf dist-test; exit $EXIT"
49
+ "test": "tsc -p tsconfig.test.json && node --test dist-test/index.test.js dist-test/resource.test.js dist-test/query-validator.test.js; EXIT=$?; rm -rf dist-test; exit $EXIT"
46
50
  }
47
51
  }