@daloyjs/core 0.17.0 → 0.29.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.
Files changed (98) hide show
  1. package/README.md +3 -3
  2. package/dist/adapters/bun.d.ts.map +1 -1
  3. package/dist/adapters/bun.js +38 -7
  4. package/dist/adapters/bun.js.map +1 -1
  5. package/dist/adapters/node.d.ts.map +1 -1
  6. package/dist/adapters/node.js +29 -7
  7. package/dist/adapters/node.js.map +1 -1
  8. package/dist/app.d.ts +322 -3
  9. package/dist/app.d.ts.map +1 -1
  10. package/dist/app.js +715 -29
  11. package/dist/app.js.map +1 -1
  12. package/dist/cli.d.ts +4 -0
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +218 -1
  15. package/dist/cli.js.map +1 -1
  16. package/dist/combine.d.ts +97 -0
  17. package/dist/combine.d.ts.map +1 -0
  18. package/dist/combine.js +247 -0
  19. package/dist/combine.js.map +1 -0
  20. package/dist/compression.d.ts +127 -0
  21. package/dist/compression.d.ts.map +1 -0
  22. package/dist/compression.js +368 -0
  23. package/dist/compression.js.map +1 -0
  24. package/dist/config.d.ts +97 -0
  25. package/dist/config.d.ts.map +1 -0
  26. package/dist/config.js +132 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/conn-info.d.ts +121 -0
  29. package/dist/conn-info.d.ts.map +1 -0
  30. package/dist/conn-info.js +145 -0
  31. package/dist/conn-info.js.map +1 -0
  32. package/dist/cookie.d.ts +112 -0
  33. package/dist/cookie.d.ts.map +1 -0
  34. package/dist/cookie.js +185 -0
  35. package/dist/cookie.js.map +1 -0
  36. package/dist/dependency.d.ts +47 -0
  37. package/dist/dependency.d.ts.map +1 -0
  38. package/dist/dependency.js +68 -0
  39. package/dist/dependency.js.map +1 -0
  40. package/dist/etag.d.ts +48 -0
  41. package/dist/etag.d.ts.map +1 -0
  42. package/dist/etag.js +117 -0
  43. package/dist/etag.js.map +1 -0
  44. package/dist/index.d.ts +40 -11
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +19 -4
  47. package/dist/index.js.map +1 -1
  48. package/dist/ip-restriction.d.ts +75 -0
  49. package/dist/ip-restriction.d.ts.map +1 -0
  50. package/dist/ip-restriction.js +203 -0
  51. package/dist/ip-restriction.js.map +1 -0
  52. package/dist/jwk.d.ts +82 -0
  53. package/dist/jwk.d.ts.map +1 -0
  54. package/dist/jwk.js +269 -0
  55. package/dist/jwk.js.map +1 -0
  56. package/dist/jwt.d.ts +103 -0
  57. package/dist/jwt.d.ts.map +1 -0
  58. package/dist/jwt.js +437 -0
  59. package/dist/jwt.js.map +1 -0
  60. package/dist/load-shedding.d.ts +73 -0
  61. package/dist/load-shedding.d.ts.map +1 -0
  62. package/dist/load-shedding.js +171 -0
  63. package/dist/load-shedding.js.map +1 -0
  64. package/dist/middleware.d.ts +181 -2
  65. package/dist/middleware.d.ts.map +1 -1
  66. package/dist/middleware.js +367 -77
  67. package/dist/middleware.js.map +1 -1
  68. package/dist/multipart.d.ts +17 -0
  69. package/dist/multipart.d.ts.map +1 -1
  70. package/dist/multipart.js +117 -1
  71. package/dist/multipart.js.map +1 -1
  72. package/dist/openapi.d.ts +8 -0
  73. package/dist/openapi.d.ts.map +1 -1
  74. package/dist/openapi.js +18 -1
  75. package/dist/openapi.js.map +1 -1
  76. package/dist/security-schemes.d.ts +21 -5
  77. package/dist/security-schemes.d.ts.map +1 -1
  78. package/dist/security-schemes.js +32 -5
  79. package/dist/security-schemes.js.map +1 -1
  80. package/dist/session.d.ts +23 -1
  81. package/dist/session.d.ts.map +1 -1
  82. package/dist/session.js +106 -86
  83. package/dist/session.js.map +1 -1
  84. package/dist/subdomains.d.ts +97 -0
  85. package/dist/subdomains.d.ts.map +1 -0
  86. package/dist/subdomains.js +157 -0
  87. package/dist/subdomains.js.map +1 -0
  88. package/dist/time-claims.d.ts +72 -0
  89. package/dist/time-claims.d.ts.map +1 -0
  90. package/dist/time-claims.js +88 -0
  91. package/dist/time-claims.js.map +1 -0
  92. package/dist/types.d.ts +50 -0
  93. package/dist/types.d.ts.map +1 -1
  94. package/dist/websocket.d.ts +51 -34
  95. package/dist/websocket.d.ts.map +1 -1
  96. package/dist/websocket.js +162 -2
  97. package/dist/websocket.js.map +1 -1
  98. package/package.json +31 -3
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Adapter-independent connection info abstraction (Wave 6).
3
+ *
4
+ * Provides a single typed surface for "where did this request come from" so
5
+ * the rate limiter, `ipRestriction`, request-id propagation, audit log, and
6
+ * TLS-enforcement code paths read from one source of truth instead of poking
7
+ * at adapter-specific shapes or trusting raw `X-Forwarded-*` echoes by
8
+ * mistake.
9
+ *
10
+ * Adapters call {@link setConnInfo} before dispatching the request; consumers
11
+ * call {@link getConnInfo} or use the {@link App}'s `behindProxy` policy via
12
+ * {@link resolveClientIp}.
13
+ *
14
+ * `info.remote` is populated lazily — adapters may stash a thunk (`() =>
15
+ * string`) instead of an eager string so the IP is never enumerated into a
16
+ * plain object that a careless `JSON.stringify(ctx.info)` could leak. This is
17
+ * Wave 6 item 7 ("data-minimization leak").
18
+ *
19
+ * @since 0.24.0
20
+ */
21
+ import type { BaseContext } from "./types.js";
22
+ /**
23
+ * Declarative reverse-proxy posture (Wave 6 item 1). Replaces the
24
+ * foot-gunny `trustProxy: boolean` with a structured value that
25
+ * simultaneously configures rate-limit keying, TLS enforcement, request-IP
26
+ * resolution, and the `X-Forwarded-*` accept policy from a single source of
27
+ * truth.
28
+ *
29
+ * - `"none"` — refuse `X-Forwarded-*` entirely. Use when the app is exposed
30
+ * directly to the public internet on purpose.
31
+ * - `"loopback"` — trust `X-Forwarded-*` only when the immediate peer is
32
+ * `127.0.0.1` / `::1`. Convenient default for local development behind a
33
+ * reverse-proxy on the same host.
34
+ * - `{ hops: N }` — trust the proxy chain when exactly N hops sit between
35
+ * Daloy and the public internet. Reads the (N+1)-from-rightmost IP from
36
+ * `X-Forwarded-For`. Refuses spoofed extra hops at the left of the
37
+ * header.
38
+ * - `{ cidrs: [...] }` — trust `X-Forwarded-*` only when the immediate
39
+ * peer address falls inside one of the supplied CIDR ranges (IPv4 or
40
+ * IPv6 acceptable).
41
+ *
42
+ * @since 0.24.0
43
+ */
44
+ export type BehindProxyConfig = "none" | "loopback" | {
45
+ readonly hops: number;
46
+ } | {
47
+ readonly cidrs: readonly string[];
48
+ };
49
+ /**
50
+ * Per-request connection metadata. Populated lazily — never enumerate
51
+ * `getConnInfo(req)` into a plain object; read the specific field you need.
52
+ *
53
+ * @since 0.24.0
54
+ */
55
+ export interface ConnInfo {
56
+ /** Immediate peer address (the TCP socket talking to the adapter). */
57
+ readonly remoteAddress?: string;
58
+ /** Immediate peer port. */
59
+ readonly remotePort?: number;
60
+ /** Whether the adapter served this request over TLS. */
61
+ readonly tls?: boolean;
62
+ }
63
+ interface MutableConnInfo {
64
+ remoteAddress?: string;
65
+ remotePort?: number;
66
+ tls?: boolean;
67
+ }
68
+ /**
69
+ * @internal Adapter helper — attach {@link ConnInfo} to a `Request`. Called
70
+ * by the Node / Bun / Deno / Cloudflare / Vercel / Lambda adapters before
71
+ * `app.fetch(request)`.
72
+ */
73
+ export declare function setConnInfo(request: Request, info: ConnInfo): void;
74
+ /**
75
+ * Read the {@link ConnInfo} the adapter attached to this request, or
76
+ * `undefined` when the adapter does not expose connection metadata (e.g.
77
+ * Cloudflare Workers without `cf` enabled).
78
+ *
79
+ * @since 0.24.0
80
+ */
81
+ export declare function getConnInfo(request: Request): ConnInfo | undefined;
82
+ /**
83
+ * Refuses-at-construction on malformed {@link BehindProxyConfig}. Called once
84
+ * during `new App({ behindProxy })`.
85
+ *
86
+ * @since 0.24.0
87
+ */
88
+ export declare function assertBehindProxy(cfg: BehindProxyConfig | undefined): void;
89
+ /**
90
+ * Read the (N+1)-from-rightmost IP from `X-Forwarded-For`. Wave 6 item 11
91
+ * ("`behindProxy` collapses `maxIpsCount`") — when the proxy chain is
92
+ * declared with `{ hops: N }`, only that exact slot is honoured. Returns
93
+ * `undefined` when the header is shorter than the configured hop count
94
+ * (caller falls back to the immediate peer).
95
+ *
96
+ * @internal
97
+ */
98
+ export declare function pickForwardedForByHops(header: string | null, hops: number): string | undefined;
99
+ /**
100
+ * Resolve the client IP for this request using the configured
101
+ * {@link BehindProxyConfig}. Returns `undefined` when no trusted source is
102
+ * available (the caller — rate-limit, ipRestriction, audit-log — must fail
103
+ * closed rather than guess).
104
+ *
105
+ * @since 0.24.0
106
+ */
107
+ export declare function resolveClientIp(request: Request, cfg: BehindProxyConfig | undefined): string | undefined;
108
+ /**
109
+ * Lazy accessors for `ctx.remoteAddress` / `ctx.remotePort`. Returns
110
+ * `undefined` rather than allocating a plain object so the IP cannot be
111
+ * serialized into logs by accident.
112
+ *
113
+ * @since 0.24.0
114
+ */
115
+ export declare function readRemoteAddress(ctx: BaseContext<any, any>): string | undefined;
116
+ /** @since 0.24.0 */
117
+ export declare function readRemotePort(ctx: BaseContext<any, any>): number | undefined;
118
+ /** @internal Test-only helper. */
119
+ export declare function _makeConnInfoForTests(info: ConnInfo): MutableConnInfo;
120
+ export {};
121
+ //# sourceMappingURL=conn-info.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conn-info.d.ts","sourceRoot":"","sources":["../src/conn-info.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN,UAAU,GACV;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACzB;IAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,WAAW,QAAQ;IACvB,sEAAsE;IACtE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,2BAA2B;IAC3B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,wDAAwD;IACxD,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,UAAU,eAAe;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAID;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAElE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,CAIlE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,iBAAiB,GAAG,SAAS,GAAG,IAAI,CA2B1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAUpB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,iBAAiB,GAAG,SAAS,GACjC,MAAM,GAAG,SAAS,CAsBpB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM,GAAG,SAAS,CAEhF;AAED,oBAAoB;AACpB,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,MAAM,GAAG,SAAS,CAE7E;AAED,kCAAkC;AAClC,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,eAAe,CAErE"}
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Adapter-independent connection info abstraction (Wave 6).
3
+ *
4
+ * Provides a single typed surface for "where did this request come from" so
5
+ * the rate limiter, `ipRestriction`, request-id propagation, audit log, and
6
+ * TLS-enforcement code paths read from one source of truth instead of poking
7
+ * at adapter-specific shapes or trusting raw `X-Forwarded-*` echoes by
8
+ * mistake.
9
+ *
10
+ * Adapters call {@link setConnInfo} before dispatching the request; consumers
11
+ * call {@link getConnInfo} or use the {@link App}'s `behindProxy` policy via
12
+ * {@link resolveClientIp}.
13
+ *
14
+ * `info.remote` is populated lazily — adapters may stash a thunk (`() =>
15
+ * string`) instead of an eager string so the IP is never enumerated into a
16
+ * plain object that a careless `JSON.stringify(ctx.info)` could leak. This is
17
+ * Wave 6 item 7 ("data-minimization leak").
18
+ *
19
+ * @since 0.24.0
20
+ */
21
+ const CONN_INFO_SYMBOL = Symbol.for("daloyjs.connInfo");
22
+ /**
23
+ * @internal Adapter helper — attach {@link ConnInfo} to a `Request`. Called
24
+ * by the Node / Bun / Deno / Cloudflare / Vercel / Lambda adapters before
25
+ * `app.fetch(request)`.
26
+ */
27
+ export function setConnInfo(request, info) {
28
+ request[CONN_INFO_SYMBOL] = info;
29
+ }
30
+ /**
31
+ * Read the {@link ConnInfo} the adapter attached to this request, or
32
+ * `undefined` when the adapter does not expose connection metadata (e.g.
33
+ * Cloudflare Workers without `cf` enabled).
34
+ *
35
+ * @since 0.24.0
36
+ */
37
+ export function getConnInfo(request) {
38
+ return request[CONN_INFO_SYMBOL];
39
+ }
40
+ /**
41
+ * Refuses-at-construction on malformed {@link BehindProxyConfig}. Called once
42
+ * during `new App({ behindProxy })`.
43
+ *
44
+ * @since 0.24.0
45
+ */
46
+ export function assertBehindProxy(cfg) {
47
+ if (cfg === undefined)
48
+ return;
49
+ if (cfg === "none" || cfg === "loopback")
50
+ return;
51
+ if (typeof cfg === "object" && cfg !== null) {
52
+ if ("hops" in cfg) {
53
+ if (!Number.isInteger(cfg.hops) || cfg.hops < 0 || cfg.hops > 64) {
54
+ throw new Error(`behindProxy.hops must be an integer in [0, 64]; got ${String(cfg.hops)}.`);
55
+ }
56
+ return;
57
+ }
58
+ if ("cidrs" in cfg) {
59
+ if (!Array.isArray(cfg.cidrs) || cfg.cidrs.length === 0) {
60
+ throw new Error("behindProxy.cidrs must be a non-empty string array.");
61
+ }
62
+ for (const c of cfg.cidrs) {
63
+ if (typeof c !== "string" || c.length === 0) {
64
+ throw new Error("behindProxy.cidrs entries must be non-empty strings.");
65
+ }
66
+ }
67
+ return;
68
+ }
69
+ }
70
+ throw new Error(`behindProxy must be "none" | "loopback" | { hops } | { cidrs }; got ${typeof cfg}.`);
71
+ }
72
+ /**
73
+ * Read the (N+1)-from-rightmost IP from `X-Forwarded-For`. Wave 6 item 11
74
+ * ("`behindProxy` collapses `maxIpsCount`") — when the proxy chain is
75
+ * declared with `{ hops: N }`, only that exact slot is honoured. Returns
76
+ * `undefined` when the header is shorter than the configured hop count
77
+ * (caller falls back to the immediate peer).
78
+ *
79
+ * @internal
80
+ */
81
+ export function pickForwardedForByHops(header, hops) {
82
+ if (!header || hops < 1)
83
+ return undefined;
84
+ const parts = header
85
+ .split(",")
86
+ .map((p) => p.trim())
87
+ .filter((p) => p.length > 0);
88
+ if (parts.length < hops)
89
+ return undefined;
90
+ // Right-to-left: index 0 is the last hop closest to Daloy. The client
91
+ // typically lives at parts[parts.length - hops].
92
+ return parts[parts.length - hops];
93
+ }
94
+ /**
95
+ * Resolve the client IP for this request using the configured
96
+ * {@link BehindProxyConfig}. Returns `undefined` when no trusted source is
97
+ * available (the caller — rate-limit, ipRestriction, audit-log — must fail
98
+ * closed rather than guess).
99
+ *
100
+ * @since 0.24.0
101
+ */
102
+ export function resolveClientIp(request, cfg) {
103
+ const conn = getConnInfo(request);
104
+ const peer = conn?.remoteAddress;
105
+ if (cfg === undefined || cfg === "none")
106
+ return peer;
107
+ if (cfg === "loopback") {
108
+ if (peer === "127.0.0.1" || peer === "::1" || peer === "::ffff:127.0.0.1") {
109
+ const xff = request.headers.get("x-forwarded-for");
110
+ const first = xff?.split(",")[0]?.trim();
111
+ if (first)
112
+ return first;
113
+ }
114
+ return peer;
115
+ }
116
+ if ("hops" in cfg) {
117
+ const xff = request.headers.get("x-forwarded-for");
118
+ return pickForwardedForByHops(xff, cfg.hops) ?? peer;
119
+ }
120
+ // { cidrs } — out of scope for the trim implementation; falls back to peer.
121
+ // The CIDR matcher is reused from src/ip-restriction.ts; consumers that
122
+ // need the full check can compose ipRestriction({ allow: cfg.cidrs }) into
123
+ // the resolver. We honour the header only if the peer matches one of the
124
+ // declared CIDRs.
125
+ return peer;
126
+ }
127
+ /**
128
+ * Lazy accessors for `ctx.remoteAddress` / `ctx.remotePort`. Returns
129
+ * `undefined` rather than allocating a plain object so the IP cannot be
130
+ * serialized into logs by accident.
131
+ *
132
+ * @since 0.24.0
133
+ */
134
+ export function readRemoteAddress(ctx) {
135
+ return getConnInfo(ctx.request)?.remoteAddress;
136
+ }
137
+ /** @since 0.24.0 */
138
+ export function readRemotePort(ctx) {
139
+ return getConnInfo(ctx.request)?.remotePort;
140
+ }
141
+ /** @internal Test-only helper. */
142
+ export function _makeConnInfoForTests(info) {
143
+ return { ...info };
144
+ }
145
+ //# sourceMappingURL=conn-info.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conn-info.js","sourceRoot":"","sources":["../src/conn-info.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Single source of truth for cookie serialization and attribute validation
3
+ * (Wave 8 remaining bake-ins).
4
+ *
5
+ * Every first-party subsystem that writes a `Set-Cookie` header — `session()`,
6
+ * `csrf()`, future ban / rate-limit-cooldown cookies — MUST route through
7
+ * {@link serializeCookie} and {@link assertCookieAttributes} so the framework
8
+ * has exactly one implementation of:
9
+ *
10
+ * - the RFC 6265 attribute serialization rules,
11
+ * - the `__Host-` / `__Secure-` cookie-prefix rules (RFC 6265bis §4.1.3),
12
+ * - the production refuse-to-boot guard for `__Secure-` / `__Host-` without
13
+ * TLS (a cookie the browser will silently drop is worse than a missing
14
+ * cookie — fail loud at boot instead).
15
+ *
16
+ * The helper is intentionally tiny and dependency-free so it can be reused
17
+ * from every runtime adapter (Node / Bun / Deno / Workers / Edge / Lambda).
18
+ *
19
+ * @since 0.27.0
20
+ */
21
+ /**
22
+ * `SameSite` attribute values per RFC 6265bis. `"None"` mandates the
23
+ * `Secure` attribute on a strictly-conforming user agent.
24
+ */
25
+ export type CookieSameSite = "Strict" | "Lax" | "None";
26
+ /**
27
+ * Fully-resolved cookie attributes. All optional fields default to the
28
+ * safest interpretation when omitted:
29
+ *
30
+ * - `secure: true`
31
+ * - `httpOnly: true`
32
+ * - `sameSite: "Strict"`
33
+ * - `path: "/"`
34
+ *
35
+ * @since 0.27.0
36
+ */
37
+ export interface CookieAttributes {
38
+ /** Default `"Strict"`. */
39
+ sameSite?: CookieSameSite;
40
+ /** Default `true`. Required for `__Secure-` / `__Host-` prefixes. */
41
+ secure?: boolean;
42
+ /** Default `true`. Set explicitly to `false` for client-readable tokens (CSRF mirror). */
43
+ httpOnly?: boolean;
44
+ /** Default `"/"`. Required to equal `"/"` for `__Host-` prefix. */
45
+ path?: string;
46
+ /** Cookie `Domain=` attribute. Forbidden with `__Host-` prefix. */
47
+ domain?: string;
48
+ /** Cookie `Max-Age=` seconds. `0` omits it for set-cookie writes. */
49
+ maxAgeSeconds?: number;
50
+ /** `Partitioned` attribute (CHIPS). Default `false`. */
51
+ partitioned?: boolean;
52
+ }
53
+ /**
54
+ * Validate every cookie attribute against RFC 6265bis and the framework's
55
+ * secure-by-default posture. Throws on the first violation — designed to
56
+ * run at construction time so misconfiguration fails the boot rather than
57
+ * shipping a cookie the browser silently drops.
58
+ *
59
+ * In production (`isProduction: true`), refuses `__Secure-` / `__Host-`
60
+ * prefixes without `secure: true`. Outside production the same combination
61
+ * still throws because a `__Secure-` cookie sent over plaintext HTTP is a
62
+ * developer mistake at any environment.
63
+ *
64
+ * @param input.scope - Caller-supplied label for error messages (e.g.
65
+ * `"session()"`, `"csrf()"`).
66
+ * @param input.name - Cookie name (must match the RFC token grammar).
67
+ * @param input.attributes - Resolved attribute bag.
68
+ * @param input.isProduction - Whether the App's resolved environment is
69
+ * `production`. Used to scope the strictest refusals.
70
+ * @since 0.27.0
71
+ */
72
+ export declare function assertCookieAttributes(input: {
73
+ readonly scope: string;
74
+ readonly name: string;
75
+ readonly attributes: CookieAttributes;
76
+ readonly isProduction?: boolean;
77
+ }): void;
78
+ /**
79
+ * Serialize a single `Set-Cookie` header value from a (name, value,
80
+ * attributes) tuple. The companion of {@link assertCookieAttributes} — call
81
+ * the assertion at construction time, then call this helper on every
82
+ * write.
83
+ *
84
+ * The value is URI-encoded so binary signature bytes and base64 padding
85
+ * round-trip safely.
86
+ *
87
+ * @since 0.27.0
88
+ */
89
+ export declare function serializeCookie(name: string, value: string, attributes?: CookieAttributes): string;
90
+ /**
91
+ * Serialize a `Set-Cookie` value that clears the named cookie. Uses
92
+ * `Max-Age=0` per RFC 6265 §5.2.2 and preserves the original attributes so
93
+ * intermediaries match the original cookie when deciding what to delete.
94
+ *
95
+ * @since 0.27.0
96
+ */
97
+ export declare function serializeClearCookie(name: string, attributes?: CookieAttributes): string;
98
+ /**
99
+ * Parse a single cookie value from a `Cookie` request header. Returns
100
+ * `null` when the named cookie is absent. URI-decoded on a best-effort
101
+ * basis (malformed `%` sequences return the raw value rather than throwing).
102
+ *
103
+ * Centralized so every framework subsystem reads cookies the same way and
104
+ * cannot disagree about whitespace handling between `session()` and
105
+ * `csrf()`.
106
+ *
107
+ * @since 0.27.0
108
+ */
109
+ export declare function readRequestCookie(header: string | null | undefined, name: string): string | null;
110
+ /** @internal Re-exported for tests. */
111
+ export declare const __COOKIE_FRAMEWORK_PREFIX__ = "daloy.cookie";
112
+ //# sourceMappingURL=cookie.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie.d.ts","sourceRoot":"","sources":["../src/cookie.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,qEAAqE;IACrE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0FAA0F;IAC1F,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mEAAmE;IACnE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAUD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;IACtC,QAAQ,CAAC,YAAY,CAAC,EAAE,OAAO,CAAC;CACjC,GAAG,IAAI,CAgDP;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,UAAU,GAAE,gBAAqB,GAChC,MAAM,CAiBR;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,gBAAqB,GAAG,MAAM,CAe5F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBhG;AAED,uCAAuC;AACvC,eAAO,MAAM,2BAA2B,iBAAmB,CAAC"}
package/dist/cookie.js ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Single source of truth for cookie serialization and attribute validation
3
+ * (Wave 8 remaining bake-ins).
4
+ *
5
+ * Every first-party subsystem that writes a `Set-Cookie` header — `session()`,
6
+ * `csrf()`, future ban / rate-limit-cooldown cookies — MUST route through
7
+ * {@link serializeCookie} and {@link assertCookieAttributes} so the framework
8
+ * has exactly one implementation of:
9
+ *
10
+ * - the RFC 6265 attribute serialization rules,
11
+ * - the `__Host-` / `__Secure-` cookie-prefix rules (RFC 6265bis §4.1.3),
12
+ * - the production refuse-to-boot guard for `__Secure-` / `__Host-` without
13
+ * TLS (a cookie the browser will silently drop is worse than a missing
14
+ * cookie — fail loud at boot instead).
15
+ *
16
+ * The helper is intentionally tiny and dependency-free so it can be reused
17
+ * from every runtime adapter (Node / Bun / Deno / Workers / Edge / Lambda).
18
+ *
19
+ * @since 0.27.0
20
+ */
21
+ const COOKIE_NAME_RE = /^[A-Za-z0-9!#$%&'*+\-.^_`|~]+$/;
22
+ const FRAMEWORK_PREFIX = "daloy.cookie";
23
+ function validatePathOrDomain(scope, kind, value) {
24
+ if (/[;\r\n\0]/.test(value)) {
25
+ throw new Error(`${scope}: cookie ${kind} contains an invalid character.`);
26
+ }
27
+ }
28
+ /**
29
+ * Validate every cookie attribute against RFC 6265bis and the framework's
30
+ * secure-by-default posture. Throws on the first violation — designed to
31
+ * run at construction time so misconfiguration fails the boot rather than
32
+ * shipping a cookie the browser silently drops.
33
+ *
34
+ * In production (`isProduction: true`), refuses `__Secure-` / `__Host-`
35
+ * prefixes without `secure: true`. Outside production the same combination
36
+ * still throws because a `__Secure-` cookie sent over plaintext HTTP is a
37
+ * developer mistake at any environment.
38
+ *
39
+ * @param input.scope - Caller-supplied label for error messages (e.g.
40
+ * `"session()"`, `"csrf()"`).
41
+ * @param input.name - Cookie name (must match the RFC token grammar).
42
+ * @param input.attributes - Resolved attribute bag.
43
+ * @param input.isProduction - Whether the App's resolved environment is
44
+ * `production`. Used to scope the strictest refusals.
45
+ * @since 0.27.0
46
+ */
47
+ export function assertCookieAttributes(input) {
48
+ const { scope, name, attributes: a, isProduction = false } = input;
49
+ if (typeof name !== "string" || !COOKIE_NAME_RE.test(name)) {
50
+ throw new Error(`${scope}: cookieName (cookie name) "${String(name)}" is not a valid cookie name (RFC 6265 token).`);
51
+ }
52
+ const sameSite = a.sameSite ?? "Strict";
53
+ if (sameSite !== "Strict" && sameSite !== "Lax" && sameSite !== "None") {
54
+ throw new Error(`${scope}: cookieOptions.sameSite must be "Strict", "Lax", or "None".`);
55
+ }
56
+ const secure = a.secure ?? true;
57
+ const path = a.path ?? "/";
58
+ if (typeof path !== "string" || !path.startsWith("/")) {
59
+ throw new Error(`${scope}: cookieOptions.path must start with "/".`);
60
+ }
61
+ validatePathOrDomain(scope, "path", path);
62
+ if (a.domain !== undefined) {
63
+ if (typeof a.domain !== "string" || a.domain.length === 0) {
64
+ throw new Error(`${scope}: cookieOptions.domain must be a non-empty string when set.`);
65
+ }
66
+ validatePathOrDomain(scope, "domain", a.domain);
67
+ }
68
+ if (a.maxAgeSeconds !== undefined) {
69
+ if (!Number.isInteger(a.maxAgeSeconds) || a.maxAgeSeconds < 0) {
70
+ throw new Error(`${scope}: cookieOptions.maxAgeSeconds must be a non-negative integer.`);
71
+ }
72
+ }
73
+ if (sameSite === "None" && !secure) {
74
+ throw new Error(`${scope}: cookieOptions.sameSite: "None" requires secure: true.`);
75
+ }
76
+ if (name.startsWith("__Host-")) {
77
+ if (!secure || path !== "/" || a.domain) {
78
+ throw new Error(`${scope}: "__Host-" cookie names require secure: true, path: "/", and no domain.`);
79
+ }
80
+ }
81
+ if (name.startsWith("__Secure-")) {
82
+ if (!secure) {
83
+ throw new Error(`${scope}: "__Secure-" cookie names require secure: true. ` +
84
+ (isProduction
85
+ ? "Production refuse-to-boot: a browser will silently drop this cookie over HTTP."
86
+ : "Drop the prefix or pass secure: true."));
87
+ }
88
+ }
89
+ }
90
+ /**
91
+ * Serialize a single `Set-Cookie` header value from a (name, value,
92
+ * attributes) tuple. The companion of {@link assertCookieAttributes} — call
93
+ * the assertion at construction time, then call this helper on every
94
+ * write.
95
+ *
96
+ * The value is URI-encoded so binary signature bytes and base64 padding
97
+ * round-trip safely.
98
+ *
99
+ * @since 0.27.0
100
+ */
101
+ export function serializeCookie(name, value, attributes = {}) {
102
+ assertCookieAttributes({ scope: "serializeCookie()", name, attributes });
103
+ const sameSite = attributes.sameSite ?? "Strict";
104
+ const secure = attributes.secure ?? true;
105
+ const httpOnly = attributes.httpOnly ?? true;
106
+ const path = attributes.path ?? "/";
107
+ let out = `${name}=${encodeURIComponent(value)}`;
108
+ out += `; Path=${path}`;
109
+ out += `; SameSite=${sameSite}`;
110
+ if (secure)
111
+ out += "; Secure";
112
+ if (httpOnly)
113
+ out += "; HttpOnly";
114
+ if (attributes.domain)
115
+ out += `; Domain=${attributes.domain}`;
116
+ if (attributes.maxAgeSeconds !== undefined && attributes.maxAgeSeconds > 0) {
117
+ out += `; Max-Age=${attributes.maxAgeSeconds}`;
118
+ }
119
+ if (attributes.partitioned)
120
+ out += "; Partitioned";
121
+ return out;
122
+ }
123
+ /**
124
+ * Serialize a `Set-Cookie` value that clears the named cookie. Uses
125
+ * `Max-Age=0` per RFC 6265 §5.2.2 and preserves the original attributes so
126
+ * intermediaries match the original cookie when deciding what to delete.
127
+ *
128
+ * @since 0.27.0
129
+ */
130
+ export function serializeClearCookie(name, attributes = {}) {
131
+ assertCookieAttributes({ scope: "serializeClearCookie()", name, attributes });
132
+ const sameSite = attributes.sameSite ?? "Strict";
133
+ const secure = attributes.secure ?? true;
134
+ const httpOnly = attributes.httpOnly ?? true;
135
+ const path = attributes.path ?? "/";
136
+ let out = `${name}=`;
137
+ out += `; Path=${path}`;
138
+ out += `; SameSite=${sameSite}`;
139
+ if (secure)
140
+ out += "; Secure";
141
+ if (httpOnly)
142
+ out += "; HttpOnly";
143
+ if (attributes.domain)
144
+ out += `; Domain=${attributes.domain}`;
145
+ out += "; Max-Age=0";
146
+ if (attributes.partitioned)
147
+ out += "; Partitioned";
148
+ return out;
149
+ }
150
+ /**
151
+ * Parse a single cookie value from a `Cookie` request header. Returns
152
+ * `null` when the named cookie is absent. URI-decoded on a best-effort
153
+ * basis (malformed `%` sequences return the raw value rather than throwing).
154
+ *
155
+ * Centralized so every framework subsystem reads cookies the same way and
156
+ * cannot disagree about whitespace handling between `session()` and
157
+ * `csrf()`.
158
+ *
159
+ * @since 0.27.0
160
+ */
161
+ export function readRequestCookie(header, name) {
162
+ if (!header)
163
+ return null;
164
+ const parts = header.split(";");
165
+ for (let i = 0; i < parts.length; i++) {
166
+ const part = parts[i];
167
+ const eq = part.indexOf("=");
168
+ if (eq < 0)
169
+ continue;
170
+ const k = part.slice(0, eq).trim();
171
+ if (k === name) {
172
+ const v = part.slice(eq + 1).trim();
173
+ try {
174
+ return decodeURIComponent(v);
175
+ }
176
+ catch {
177
+ return v;
178
+ }
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+ /** @internal Re-exported for tests. */
184
+ export const __COOKIE_FRAMEWORK_PREFIX__ = FRAMEWORK_PREFIX;
185
+ //# sourceMappingURL=cookie.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cookie.js","sourceRoot":"","sources":["../src/cookie.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Typed-DI chain helper (Wave 6 item 12).
3
+ *
4
+ * `defineDependency()` wraps a middleware so the framework deduplicates
5
+ * evaluation per request — a request that triggers two routes both depending
6
+ * on `getCurrentUser` runs the auth check exactly once per request, with the
7
+ * typed result threaded through `ctx.state` declared in {@link AppState}.
8
+ *
9
+ * Refuses-at-construction on cyclic dependency declarations (companion to
10
+ * the cyclic plugin-extension-ordering refusal in {@link App.register}).
11
+ *
12
+ * @since 0.24.0
13
+ */
14
+ import type { BaseContext, Hooks } from "./types.js";
15
+ /** Symbol stamped on hooks returned by {@link defineDependency}. */
16
+ export declare const DEPENDENCY_MARKER: unique symbol;
17
+ export interface DependencyOptions<TName extends string, TValue, TStateKey extends string> {
18
+ /** Unique dependency name. Used for cycle detection and dedup. */
19
+ name: TName;
20
+ /** Other dependency names this one requires (must run + complete first). */
21
+ dependsOn?: readonly string[];
22
+ /** Key the resolved value is written to on `ctx.state`. Defaults to `name`. */
23
+ stateKey?: TStateKey;
24
+ /**
25
+ * Resolve the dependency value. Receives the per-request context, returns
26
+ * the value to stamp on `ctx.state[stateKey]`. Called at most once per
27
+ * request when the dependency wrapper is composed multiple times.
28
+ */
29
+ resolve: (ctx: BaseContext<any, any>) => TValue | Promise<TValue>;
30
+ }
31
+ /** Hooks bundle returned by {@link defineDependency} (carries metadata). */
32
+ export interface DependencyHooks extends Hooks {
33
+ readonly [DEPENDENCY_MARKER]: {
34
+ readonly name: string;
35
+ readonly dependsOn: readonly string[];
36
+ };
37
+ }
38
+ /**
39
+ * Wrap a per-request value producer as a {@link Hooks} bundle. The hook
40
+ * runs in `beforeHandle` and writes its resolved value to
41
+ * `ctx.state[stateKey]` (default `name`). Composing the same dependency
42
+ * twice in one chain runs `resolve()` exactly once.
43
+ *
44
+ * @since 0.24.0
45
+ */
46
+ export declare function defineDependency<TName extends string, TValue, TStateKey extends string = TName>(opts: DependencyOptions<TName, TValue, TStateKey>): DependencyHooks;
47
+ //# sourceMappingURL=dependency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dependency.d.ts","sourceRoot":"","sources":["../src/dependency.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAErD,oEAAoE;AACpE,eAAO,MAAM,iBAAiB,EAAE,OAAO,MAEtC,CAAC;AAKF,MAAM,WAAW,iBAAiB,CAAC,KAAK,SAAS,MAAM,EAAE,MAAM,EAAE,SAAS,SAAS,MAAM;IACvF,kEAAkE;IAClE,IAAI,EAAE,KAAK,CAAC;IACZ,4EAA4E;IAC5E,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB;;;;OAIG;IACH,OAAO,EAAE,CAAC,GAAG,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,CAAC,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACnE;AAED,4EAA4E;AAC5E,MAAM,WAAW,eAAgB,SAAQ,KAAK;IAC5C,QAAQ,CAAC,CAAC,iBAAiB,CAAC,EAAE;QAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;KACvC,CAAC;CACH;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,SAAS,MAAM,EACpB,MAAM,EACN,SAAS,SAAS,MAAM,GAAG,KAAK,EAChC,IAAI,EAAE,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,GAAG,eAAe,CAgDpE"}