Skip to content

Commit b200648

Browse files
committed
server-cookies: add encrypt+sign and tests
1 parent 947f105 commit b200648

File tree

6 files changed

+585
-105
lines changed

6 files changed

+585
-105
lines changed

packages/server-cookies/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
"@alepha/server": "0.7.7"
2020
},
2121
"devDependencies": {
22-
"tsdown": "^0.12.9"
22+
"tsdown": "^0.12.9",
23+
"vitest": "^3.2.4"
2324
},
2425
"scripts": {
2526
"build": "tsdown -c ../../tsdown.config.ts"

packages/server-cookies/src/descriptors/$cookie.ts

Lines changed: 48 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1,142 +1,100 @@
1-
import { deflateRawSync, inflateRawSync } from "node:zlib";
21
import {
32
__descriptor,
4-
$cursor,
53
KIND,
4+
NotImplementedError,
65
OPTIONS,
76
type Static,
87
type TSchema,
98
} from "@alepha/core";
10-
import { DateTimeProvider, type DurationLike } from "@alepha/datetime";
11-
import type { ServerRequest } from "@alepha/server";
9+
import type { DurationLike } from "@alepha/datetime";
10+
11+
const KEY = "COOKIE";
1212

1313
export interface CookieDescriptorOptions<T extends TSchema> {
14+
/** The schema for the cookie's value, used for validation and type safety. */
1415
schema: T;
1516

16-
name: string;
17+
/** The name of the cookie. */
18+
name?: string;
1719

18-
path?: string; // default: "/"
20+
/** The cookie's path. Defaults to "/". */
21+
path?: string;
1922

20-
ttl?: DurationLike; // map to maxAge
23+
/** Time-to-live for the cookie. Maps to `Max-Age`. */
24+
ttl?: DurationLike;
2125

22-
secure?: boolean; // TODO: "auto" - secure=true if ctx.url.protocol === "https"
26+
/** If true, the cookie is only sent over HTTPS. Defaults to true in production. */
27+
secure?: boolean;
2328

29+
/** If true, the cookie cannot be accessed by client-side scripts. */
2430
httpOnly?: boolean;
2531

26-
sameSite?: "strict" | "lax" | "none"; // default: "lax"
32+
/** SameSite policy for the cookie. Defaults to "lax". */
33+
sameSite?: "strict" | "lax" | "none";
2734

35+
/** The domain for the cookie. */
2836
domain?: string;
2937

38+
/** If true, the cookie value will be compressed using zlib. */
3039
compress?: boolean;
3140

32-
encrypt?: boolean; // not implemented yet
41+
/** If true, the cookie value will be encrypted. Requires `COOKIE_SECRET` env var. */
42+
encrypt?: boolean;
3343

34-
sign?: boolean; // not implemented yet
44+
/** If true, the cookie will be signed to prevent tampering. Requires `COOKIE_SECRET` env var. */
45+
sign?: boolean;
3546
}
3647

3748
export interface CookieDescriptor<T extends TSchema> {
38-
[KIND]: "COOKIE";
39-
49+
[KIND]: typeof KEY;
4050
[OPTIONS]: CookieDescriptorOptions<T>;
4151

52+
schema: T;
53+
54+
/** Sets the cookie with the given value in the current request's response. */
4255
set: (value: Static<T>, options?: { cookies?: Cookies }) => void;
4356

57+
/** Gets the cookie value from the current request. Returns undefined if not found or invalid. */
4458
get: (options?: { cookies?: Cookies }) => Static<T> | undefined;
4559

60+
/** Deletes the cookie in the current request's response. */
4661
del: (options?: { cookies?: Cookies }) => void;
4762
}
4863

64+
/**
65+
* Declares a type-safe, configurable HTTP cookie.
66+
* This descriptor provides methods to get, set, and delete the cookie
67+
* within the server request/response cycle.
68+
*/
4969
export const $cookie: {
5070
<T extends TSchema>(options: CookieDescriptorOptions<T>): CookieDescriptor<T>;
5171
[KIND]: string;
5272
} = <T extends TSchema>(
5373
options: CookieDescriptorOptions<T>,
5474
): CookieDescriptor<T> => {
55-
__descriptor("COOKIE");
75+
__descriptor(KEY);
5676

57-
const { context: alepha } = $cursor();
58-
59-
return {
60-
[KIND]: "COOKIE",
77+
const api: Partial<CookieDescriptor<T>> = {
78+
[KIND]: KEY,
6179
[OPTIONS]: options,
62-
get: (opts: { cookies?: Cookies } = {}) => {
63-
const cookies =
64-
alepha.context.get<ServerRequest>("request")?.cookies ?? opts.cookies;
65-
if (!cookies) {
66-
throw new Error(
67-
"Cookies not found in request context or options.cookies",
68-
);
69-
}
70-
71-
try {
72-
if (cookies.req[options.name]) {
73-
let value: string = decodeURIComponent(cookies.req[options.name]);
74-
75-
if (options.compress) {
76-
value = inflateRawSync(Buffer.from(value, "base64")).toString(
77-
"utf8",
78-
);
79-
}
80-
81-
return alepha.parse(options.schema, JSON.parse(value));
82-
}
83-
} catch (e) {
84-
alepha.log.error(e);
85-
cookies.res[options.name] = null;
86-
}
87-
88-
return undefined;
80+
schema: options.schema,
81+
set: () => {
82+
throw new NotImplementedError(KEY);
8983
},
90-
91-
del: (opts: { cookies?: Cookies } = {}) => {
92-
const cookies =
93-
alepha.context.get<ServerRequest>("request")?.cookies ?? opts.cookies;
94-
if (!cookies) {
95-
throw new Error(
96-
"Cookies not found in request context or options.cookies",
97-
);
98-
}
99-
100-
cookies.res[options.name] = null;
84+
get: () => {
85+
throw new NotImplementedError(KEY);
10186
},
102-
103-
set: (data: Static<T>, opts: { cookies?: Cookies } = {}) => {
104-
const cookies =
105-
alepha.context.get<ServerRequest>("request")?.cookies ?? opts.cookies;
106-
if (!cookies) {
107-
throw new Error(
108-
"Cookies not found in request context or options.cookies",
109-
);
110-
}
111-
112-
let value = JSON.stringify(alepha.parse(options.schema, data));
113-
114-
if (options.compress) {
115-
value = deflateRawSync(value).toString("base64");
116-
}
117-
118-
value = encodeURIComponent(value);
119-
120-
const cookie: Cookie = {
121-
value,
122-
path: options.path ?? "/",
123-
sameSite: options.sameSite ?? "lax",
124-
secure: options.secure,
125-
httpOnly: options.httpOnly,
126-
domain: options.domain,
127-
};
128-
129-
if (options.ttl) {
130-
const dt = alepha.get(DateTimeProvider);
131-
cookie.maxAge = dt.duration(options.ttl).as("seconds");
132-
}
133-
134-
cookies.res[options.name] = cookie;
87+
del: () => {
88+
throw new NotImplementedError(KEY);
13589
},
13690
};
91+
92+
return api as CookieDescriptor<T>;
13793
};
13894

139-
$cookie[KIND] = "COOKIE";
95+
$cookie[KIND] = KEY;
96+
97+
// ---------------------------------------------------------------------------------------------------------------------
14098

14199
export interface Cookies {
142100
req: Record<string, string>;

0 commit comments

Comments
 (0)