@scayle/h3-session 0.4.1 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @scayle/h3-session
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Cookie metadata is no longer saved in the session store. Previously, `h3-session` stored a copy of the cookie settings in the session store alongside the session data. This has been changed and now only the session data itself is stored. The cookie settings can still be read and manipulated from the `Session` object.
8
+
9
+ ### Patch Changes
10
+
11
+ - Removed dependency `zod@^3.23.8`
12
+ - Replace regex used for cookie parsing
13
+
14
+ ## 0.4.2
15
+
16
+ ### Patch Changes
17
+
18
+ - Build the package with `unbuild`
19
+
3
20
  ## 0.4.1
4
21
 
5
22
  ### Patch Changes
@@ -0,0 +1,143 @@
1
+ import { setCookie, H3Event } from 'h3';
2
+ import { Storage } from 'unstorage';
3
+
4
+ interface SessionMethods {
5
+ save: () => Promise<void>;
6
+ reload: () => Promise<void>;
7
+ destroy: () => Promise<void>;
8
+ regenerate: () => Promise<void>;
9
+ }
10
+ declare class Session implements SessionMethods {
11
+ #private;
12
+ data: SessionDataT;
13
+ cookie: SessionCookie;
14
+ constructor(id: string, data: SessionDataT, store: SessionStore, generate: () => Promise<{
15
+ id: string;
16
+ data: SessionDataT;
17
+ }>, cookie: SessionCookie);
18
+ get id(): string;
19
+ save(): Promise<void>;
20
+ reload(): Promise<void>;
21
+ destroy(): Promise<void>;
22
+ regenerate(): Promise<void>;
23
+ }
24
+
25
+ type TTL = number | ((data: RawSession) => number);
26
+ interface UnstorageSessionStoreOptions {
27
+ prefix: string;
28
+ ttl: TTL;
29
+ }
30
+ declare class UnstorageSessionStore implements SessionStore {
31
+ storage: Storage<RawSession>;
32
+ prefix: string;
33
+ ttl: TTL;
34
+ constructor(storage: Storage, options?: Partial<UnstorageSessionStoreOptions>);
35
+ /**
36
+ * Destroy the session with the specified session ID
37
+ * @param sid the un-prefixed session ID
38
+ */
39
+ destroy(sid: string): Promise<void>;
40
+ /**
41
+ * Get the session with the specified session ID
42
+ * @param sid the un-prefixed session ID
43
+ */
44
+ get(sid: string): Promise<SessionDataT | undefined>;
45
+ /**
46
+ * Save the session with the specified session ID
47
+ * If the session has expired (has a TTL <= 0) it is deleted.
48
+ * @param sid the un-prefixed session ID
49
+ * @param data the session data
50
+ */
51
+ set(sid: string, data: RawSession): Promise<void>;
52
+ /**
53
+ * Update a session's TTL
54
+ * @param sid the un-prefixed session ID
55
+ * @param data the session data
56
+ */
57
+ touch(sid: string, data: RawSession): Promise<void>;
58
+ /**
59
+ * Remove all saved sessions
60
+ */
61
+ clear(): Promise<void>;
62
+ /**
63
+ * Fetch all saved sessions.
64
+ */
65
+ all(): Promise<SessionDataT[]>;
66
+ /**
67
+ * Returns the number of saved sessions
68
+ */
69
+ length(): Promise<number>;
70
+ private getAllKeys;
71
+ private getKey;
72
+ /**
73
+ * Get the TTL for the session, either from the TTL function if it exists
74
+ * or falling back to the default TTL value.
75
+ * @param session
76
+ * @private
77
+ */
78
+ private getTTL;
79
+ }
80
+
81
+ type SessionCookieOptions = Parameters<typeof setCookie>[3];
82
+ type SessionCookie = SessionCookieOptions & {
83
+ setSessionId: (sid: string) => Promise<void>;
84
+ };
85
+ interface SessionDataT {
86
+ }
87
+ type RawSession = SessionDataT;
88
+ interface SessionStore {
89
+ all?: () => Promise<RawSession[]>;
90
+ destroy: (sid: string) => Promise<void>;
91
+ clear?: () => Promise<void>;
92
+ length?: () => Promise<number>;
93
+ get: (sid: string) => Promise<RawSession | undefined>;
94
+ set: (sid: string, data: RawSession) => Promise<void>;
95
+ touch: (sid: string, data: SessionDataT) => Promise<void>;
96
+ }
97
+ interface H3SessionOptions {
98
+ store: SessionStore;
99
+ cookie?: {
100
+ domain?: string;
101
+ expires?: Date;
102
+ httpOnly?: boolean;
103
+ maxAge?: number;
104
+ path?: string;
105
+ sameSite?: true | false | 'lax' | 'none' | 'strict';
106
+ secure?: boolean;
107
+ };
108
+ name?: string;
109
+ genid?: (event: H3Event) => string;
110
+ generate?: () => SessionDataT;
111
+ saveUninitialized?: boolean;
112
+ secret: string | string[];
113
+ }
114
+ declare module 'h3' {
115
+ interface H3EventContext {
116
+ session: Session;
117
+ sessionId: string;
118
+ sessionStore: SessionStore;
119
+ }
120
+ }
121
+ /**
122
+ * Verify that a cookie was signed with one of the secrets
123
+ * If it's valid, return the embedded message
124
+ *
125
+ * @param value a cookie value in the format `s:[value].[signature]`
126
+ * @param secrets an array of secret strings to verify with
127
+ */
128
+ declare function unsignCookie(value: string, secrets: string[]): Promise<string | false>;
129
+ /**
130
+ * Creates a signed cookie value in the format `s:[value].[signature]`
131
+ * @param value
132
+ * @param secret
133
+ */
134
+ declare function signCookie(value: string, secret: string): Promise<string>;
135
+ declare function validateConfig(config: H3SessionOptions): void;
136
+ /**
137
+ * Attach a session to an H3Event
138
+ * @param event
139
+ * @param config
140
+ */
141
+ declare function useSession(event: H3Event, config: H3SessionOptions): Promise<void>;
142
+
143
+ export { type H3SessionOptions, type RawSession, Session, type SessionCookie, type SessionCookieOptions, type SessionDataT, type SessionMethods, type SessionStore, UnstorageSessionStore, signCookie, unsignCookie, useSession, validateConfig };
@@ -0,0 +1,143 @@
1
+ import { setCookie, H3Event } from 'h3';
2
+ import { Storage } from 'unstorage';
3
+
4
+ interface SessionMethods {
5
+ save: () => Promise<void>;
6
+ reload: () => Promise<void>;
7
+ destroy: () => Promise<void>;
8
+ regenerate: () => Promise<void>;
9
+ }
10
+ declare class Session implements SessionMethods {
11
+ #private;
12
+ data: SessionDataT;
13
+ cookie: SessionCookie;
14
+ constructor(id: string, data: SessionDataT, store: SessionStore, generate: () => Promise<{
15
+ id: string;
16
+ data: SessionDataT;
17
+ }>, cookie: SessionCookie);
18
+ get id(): string;
19
+ save(): Promise<void>;
20
+ reload(): Promise<void>;
21
+ destroy(): Promise<void>;
22
+ regenerate(): Promise<void>;
23
+ }
24
+
25
+ type TTL = number | ((data: RawSession) => number);
26
+ interface UnstorageSessionStoreOptions {
27
+ prefix: string;
28
+ ttl: TTL;
29
+ }
30
+ declare class UnstorageSessionStore implements SessionStore {
31
+ storage: Storage<RawSession>;
32
+ prefix: string;
33
+ ttl: TTL;
34
+ constructor(storage: Storage, options?: Partial<UnstorageSessionStoreOptions>);
35
+ /**
36
+ * Destroy the session with the specified session ID
37
+ * @param sid the un-prefixed session ID
38
+ */
39
+ destroy(sid: string): Promise<void>;
40
+ /**
41
+ * Get the session with the specified session ID
42
+ * @param sid the un-prefixed session ID
43
+ */
44
+ get(sid: string): Promise<SessionDataT | undefined>;
45
+ /**
46
+ * Save the session with the specified session ID
47
+ * If the session has expired (has a TTL <= 0) it is deleted.
48
+ * @param sid the un-prefixed session ID
49
+ * @param data the session data
50
+ */
51
+ set(sid: string, data: RawSession): Promise<void>;
52
+ /**
53
+ * Update a session's TTL
54
+ * @param sid the un-prefixed session ID
55
+ * @param data the session data
56
+ */
57
+ touch(sid: string, data: RawSession): Promise<void>;
58
+ /**
59
+ * Remove all saved sessions
60
+ */
61
+ clear(): Promise<void>;
62
+ /**
63
+ * Fetch all saved sessions.
64
+ */
65
+ all(): Promise<SessionDataT[]>;
66
+ /**
67
+ * Returns the number of saved sessions
68
+ */
69
+ length(): Promise<number>;
70
+ private getAllKeys;
71
+ private getKey;
72
+ /**
73
+ * Get the TTL for the session, either from the TTL function if it exists
74
+ * or falling back to the default TTL value.
75
+ * @param session
76
+ * @private
77
+ */
78
+ private getTTL;
79
+ }
80
+
81
+ type SessionCookieOptions = Parameters<typeof setCookie>[3];
82
+ type SessionCookie = SessionCookieOptions & {
83
+ setSessionId: (sid: string) => Promise<void>;
84
+ };
85
+ interface SessionDataT {
86
+ }
87
+ type RawSession = SessionDataT;
88
+ interface SessionStore {
89
+ all?: () => Promise<RawSession[]>;
90
+ destroy: (sid: string) => Promise<void>;
91
+ clear?: () => Promise<void>;
92
+ length?: () => Promise<number>;
93
+ get: (sid: string) => Promise<RawSession | undefined>;
94
+ set: (sid: string, data: RawSession) => Promise<void>;
95
+ touch: (sid: string, data: SessionDataT) => Promise<void>;
96
+ }
97
+ interface H3SessionOptions {
98
+ store: SessionStore;
99
+ cookie?: {
100
+ domain?: string;
101
+ expires?: Date;
102
+ httpOnly?: boolean;
103
+ maxAge?: number;
104
+ path?: string;
105
+ sameSite?: true | false | 'lax' | 'none' | 'strict';
106
+ secure?: boolean;
107
+ };
108
+ name?: string;
109
+ genid?: (event: H3Event) => string;
110
+ generate?: () => SessionDataT;
111
+ saveUninitialized?: boolean;
112
+ secret: string | string[];
113
+ }
114
+ declare module 'h3' {
115
+ interface H3EventContext {
116
+ session: Session;
117
+ sessionId: string;
118
+ sessionStore: SessionStore;
119
+ }
120
+ }
121
+ /**
122
+ * Verify that a cookie was signed with one of the secrets
123
+ * If it's valid, return the embedded message
124
+ *
125
+ * @param value a cookie value in the format `s:[value].[signature]`
126
+ * @param secrets an array of secret strings to verify with
127
+ */
128
+ declare function unsignCookie(value: string, secrets: string[]): Promise<string | false>;
129
+ /**
130
+ * Creates a signed cookie value in the format `s:[value].[signature]`
131
+ * @param value
132
+ * @param secret
133
+ */
134
+ declare function signCookie(value: string, secret: string): Promise<string>;
135
+ declare function validateConfig(config: H3SessionOptions): void;
136
+ /**
137
+ * Attach a session to an H3Event
138
+ * @param event
139
+ * @param config
140
+ */
141
+ declare function useSession(event: H3Event, config: H3SessionOptions): Promise<void>;
142
+
143
+ export { type H3SessionOptions, type RawSession, Session, type SessionCookie, type SessionCookieOptions, type SessionDataT, type SessionMethods, type SessionStore, UnstorageSessionStore, signCookie, unsignCookie, useSession, validateConfig };
package/dist/index.mjs ADDED
@@ -0,0 +1,279 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { getCookie, setCookie } from 'h3';
3
+ import { defu } from 'defu';
4
+ import { subtle, randomUUID } from 'uncrypto';
5
+
6
+ class Session {
7
+ #id;
8
+ #store;
9
+ data;
10
+ #generate;
11
+ cookie;
12
+ constructor(id, data, store, generate, cookie) {
13
+ this.data = data;
14
+ this.#id = id;
15
+ this.#store = store;
16
+ this.#generate = generate;
17
+ this.cookie = cookie;
18
+ }
19
+ get id() {
20
+ return this.#id;
21
+ }
22
+ async save() {
23
+ await this.#store.set(this.#id, this.data);
24
+ }
25
+ async reload() {
26
+ this.data = await this.#store.get(this.#id) ?? (await this.#generate()).data;
27
+ }
28
+ async destroy() {
29
+ this.cookie.maxAge = 0;
30
+ await this.#store.destroy(this.#id);
31
+ }
32
+ async regenerate() {
33
+ await this.#store.destroy(this.#id);
34
+ const { data, id } = await this.#generate();
35
+ this.data = data;
36
+ this.#id = id;
37
+ await Promise.all([this.cookie.setSessionId(id), this.save()]);
38
+ }
39
+ }
40
+
41
+ class UnstorageSessionStore {
42
+ storage;
43
+ prefix;
44
+ ttl;
45
+ constructor(storage, options) {
46
+ this.storage = storage;
47
+ this.prefix = options?.prefix ?? "sess";
48
+ this.ttl = options?.ttl ?? 60 * 60 * 24;
49
+ }
50
+ /**
51
+ * Destroy the session with the specified session ID
52
+ * @param sid the un-prefixed session ID
53
+ */
54
+ async destroy(sid) {
55
+ await this.storage.removeItem(this.getKey(sid));
56
+ }
57
+ /**
58
+ * Get the session with the specified session ID
59
+ * @param sid the un-prefixed session ID
60
+ */
61
+ async get(sid) {
62
+ const item = await this.storage.getItem(this.getKey(sid));
63
+ return item ?? void 0;
64
+ }
65
+ /**
66
+ * Save the session with the specified session ID
67
+ * If the session has expired (has a TTL <= 0) it is deleted.
68
+ * @param sid the un-prefixed session ID
69
+ * @param data the session data
70
+ */
71
+ async set(sid, data) {
72
+ const ttl = this.getTTL(data);
73
+ if (ttl > 0) {
74
+ await this.storage.setItem(this.getKey(sid), data, { ttl });
75
+ } else {
76
+ await this.destroy(sid);
77
+ }
78
+ }
79
+ /**
80
+ * Update a session's TTL
81
+ * @param sid the un-prefixed session ID
82
+ * @param data the session data
83
+ */
84
+ async touch(sid, data) {
85
+ await this.set(sid, data);
86
+ }
87
+ /**
88
+ * Remove all saved sessions
89
+ */
90
+ async clear() {
91
+ return await this.storage.clear(this.prefix);
92
+ }
93
+ /**
94
+ * Fetch all saved sessions.
95
+ */
96
+ async all() {
97
+ const keys = await this.getAllKeys();
98
+ const values = await this.storage.getItems(keys);
99
+ return values.map(({ value }) => {
100
+ return value;
101
+ });
102
+ }
103
+ /**
104
+ * Returns the number of saved sessions
105
+ */
106
+ async length() {
107
+ const keys = await this.getAllKeys();
108
+ return keys.length;
109
+ }
110
+ async getAllKeys() {
111
+ return await this.storage.getKeys(this.prefix);
112
+ }
113
+ getKey(key) {
114
+ return [this.prefix, key].join(":");
115
+ }
116
+ /**
117
+ * Get the TTL for the session, either from the TTL function if it exists
118
+ * or falling back to the default TTL value.
119
+ * @param session
120
+ * @private
121
+ */
122
+ getTTL(session) {
123
+ if (typeof this.ttl === "function") {
124
+ return this.ttl(session);
125
+ }
126
+ return this.ttl;
127
+ }
128
+ }
129
+
130
+ const signatureLength = 43;
131
+ function parseCookieValue(value) {
132
+ if (value.length < signatureLength + 3) {
133
+ return void 0;
134
+ }
135
+ const separatorIndex = value.length - signatureLength - 1;
136
+ if (value.charAt(0) !== "s" || value.charAt(1) !== ":" || value.charAt(separatorIndex) !== ".") {
137
+ return void 0;
138
+ }
139
+ return [
140
+ value.substring(2, separatorIndex),
141
+ value.substring(separatorIndex + 1)
142
+ ];
143
+ }
144
+ async function unsignCookie(value, secrets) {
145
+ const matches = parseCookieValue(value);
146
+ if (!matches) {
147
+ return false;
148
+ }
149
+ const [message, signature] = matches;
150
+ const encoder = new TextEncoder();
151
+ const messageUint8Array = encoder.encode(message);
152
+ const signatureUint8Array = Uint8Array.from(Buffer.from(signature, "base64"));
153
+ for (let i = 0; i < secrets.length; i++) {
154
+ const keyUint8Array = encoder.encode(secrets[i]);
155
+ const cryptoKey = await subtle.importKey(
156
+ "raw",
157
+ keyUint8Array,
158
+ { name: "HMAC", hash: "SHA-256" },
159
+ false,
160
+ ["verify"]
161
+ );
162
+ const result = await subtle.verify(
163
+ "HMAC",
164
+ cryptoKey,
165
+ signatureUint8Array,
166
+ messageUint8Array
167
+ );
168
+ if (result) {
169
+ return message;
170
+ }
171
+ }
172
+ return false;
173
+ }
174
+ async function signCookie(value, secret) {
175
+ if (!value) {
176
+ throw new Error("[h3-session] No value was provided!");
177
+ }
178
+ if (!secret) {
179
+ throw new Error("[h3-session] No secret was provided!");
180
+ }
181
+ const encoder = new TextEncoder();
182
+ const messageUint8Array = encoder.encode(value);
183
+ const keyUint8Array = encoder.encode(secret);
184
+ const cryptoKey = await subtle.importKey(
185
+ "raw",
186
+ keyUint8Array,
187
+ { name: "HMAC", hash: "SHA-256" },
188
+ false,
189
+ ["sign"]
190
+ );
191
+ const signature = await subtle.sign("HMAC", cryptoKey, messageUint8Array);
192
+ const b64Signature = Buffer.from(signature).toString("base64").replace(/=+$/, "");
193
+ return `s:${value}.${b64Signature}`;
194
+ }
195
+ function validateConfig(config) {
196
+ if (!config.store) {
197
+ throw new Error("[h3-session] Session store is required!");
198
+ }
199
+ if (!config.secret) {
200
+ throw new Error("[h3-session] Session secret is required!");
201
+ }
202
+ }
203
+ async function useSession(event, config) {
204
+ if (event.context.session) {
205
+ return;
206
+ }
207
+ validateConfig(config);
208
+ const sessionConfig = defu(config, {
209
+ name: "connect.sid",
210
+ genid: (_event) => {
211
+ return randomUUID();
212
+ },
213
+ generate: () => ({}),
214
+ cookie: { path: "/", httpOnly: true, secure: true, maxAge: null },
215
+ saveUninitialized: false
216
+ });
217
+ const { store } = sessionConfig;
218
+ const generate = async () => {
219
+ return await Promise.resolve({
220
+ id: sessionConfig.genid(event),
221
+ data: sessionConfig.generate()
222
+ });
223
+ };
224
+ const normalizedSecrets = Array.isArray(sessionConfig.secret) ? sessionConfig.secret : [sessionConfig.secret];
225
+ const createSessionCookie = async (sid) => {
226
+ let signedCookie;
227
+ const cookie = {
228
+ ...sessionConfig.cookie,
229
+ // Default to a max age of one day
230
+ maxAge: sessionConfig.cookie.maxAge || 60 * 60 * 24,
231
+ setSessionId: async (sid2) => {
232
+ signedCookie = await signCookie(
233
+ sid2,
234
+ normalizedSecrets[normalizedSecrets.length - 1]
235
+ );
236
+ setCookie(event, sessionConfig.name, signedCookie, cookie);
237
+ }
238
+ };
239
+ await cookie.setSessionId(sid);
240
+ return new Proxy(cookie, {
241
+ set(target, property, value) {
242
+ target[property] = value;
243
+ setCookie(event, sessionConfig.name, signedCookie, cookie);
244
+ return true;
245
+ }
246
+ });
247
+ };
248
+ const rawCookie = getCookie(event, sessionConfig.name);
249
+ const sessionId = rawCookie ? await unsignCookie(rawCookie, normalizedSecrets) : null;
250
+ let sessionData;
251
+ if (sessionId) {
252
+ sessionData = await store.get(sessionId);
253
+ }
254
+ async function createNewSession() {
255
+ const { id, data } = await generate();
256
+ const cookie = await createSessionCookie(id);
257
+ event.context.session = new Session(id, data, store, generate, cookie);
258
+ if (sessionConfig.saveUninitialized) {
259
+ await event.context.session.save();
260
+ }
261
+ event.context.sessionId = id;
262
+ }
263
+ async function createExistingSession(id, data) {
264
+ const cookie = await createSessionCookie(id);
265
+ if (store.touch) {
266
+ await store.touch(id, data);
267
+ }
268
+ event.context.session = new Session(id, data, store, generate, cookie);
269
+ event.context.sessionId = id;
270
+ }
271
+ if (!sessionId || !sessionData) {
272
+ await createNewSession();
273
+ } else {
274
+ await createExistingSession(sessionId, sessionData);
275
+ }
276
+ event.context.sessionStore = store;
277
+ }
278
+
279
+ export { Session, UnstorageSessionStore, signCookie, unsignCookie, useSession, validateConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/h3-session",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Persistent sessions for h3",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -9,19 +9,24 @@
9
9
  "registry": "https://registry.npmjs.org/"
10
10
  },
11
11
  "exports": {
12
- ".": "./dist/index.js"
12
+ ".": "./dist/index.mjs"
13
13
  },
14
- "main": "./dist/index.js",
14
+ "main": "./dist/index.mjs",
15
15
  "files": [
16
16
  "CHANGELOG.md",
17
- "./dist/*.js"
17
+ "dist/**"
18
18
  ],
19
19
  "scripts": {
20
- "build": "yarn tsc -p tsconfig.json",
20
+ "build": "unbuild",
21
+ "verify-packaging": "attw --pack . --profile esm-only",
21
22
  "format": "dprint check",
22
23
  "format:fix": "dprint fmt",
23
24
  "lint": "eslint . --format gitlab",
24
- "lint:fix": "eslint . --fix"
25
+ "lint:fix": "eslint . --fix",
26
+ "test": "vitest run",
27
+ "test:ci": "vitest --run --coverage --reporter=default --reporter=junit --outputFile=./junit.xml",
28
+ "test:watch": "vitest watch",
29
+ "typecheck": "tsc --noEmit -p tsconfig.json"
25
30
  },
26
31
  "peerDependencies": {
27
32
  "h3": "^1.10.0"
@@ -29,18 +34,20 @@
29
34
  "dependencies": {
30
35
  "defu": "^6.1.4",
31
36
  "uncrypto": "^0.1.3",
32
- "unstorage": "^1.10.2",
33
- "zod": "^3.23.8"
37
+ "unstorage": "^1.10.2"
34
38
  },
35
39
  "devDependencies": {
36
- "@scayle/eslint-config-storefront": "4.3.1",
37
- "dprint": "0.47.2",
38
- "eslint": "9.12.0",
40
+ "@arethetypeswrong/cli": "0.17.1",
41
+ "@scayle/eslint-config-storefront": "4.4.0",
42
+ "dprint": "0.47.6",
43
+ "eslint": "9.17.0",
39
44
  "eslint-formatter-gitlab": "5.1.0",
40
45
  "h3": "1.13.0",
41
- "typescript": "5.6.3"
46
+ "typescript": "5.6.3",
47
+ "unbuild": "3.0.1",
48
+ "vitest": "2.1.8"
42
49
  },
43
50
  "volta": {
44
- "node": "20.18.0"
51
+ "node": "22.12.0"
45
52
  }
46
53
  }
package/dist/index.js DELETED
@@ -1,159 +0,0 @@
1
- // A session implementation for h3 based on express-session
2
- import { Buffer } from 'node:buffer';
3
- import { getCookie, setCookie } from 'h3';
4
- import { defu } from 'defu';
5
- import { randomUUID, subtle } from 'uncrypto';
6
- import { Session } from './session';
7
- /**
8
- * Verify that a cookie was signed with one of the secrets
9
- * If it's valid, return the embedded message
10
- *
11
- * @param value a cookie value in the format `s:[value].[signature]`
12
- * @param secrets an array of secret strings to verify with
13
- */
14
- export async function unsignCookie(value, secrets) {
15
- // Validate cookie format
16
- const matches = /s:([^.]*)\.(.*)/.exec(value);
17
- if (!matches) {
18
- return false;
19
- }
20
- const [, message, signature] = matches;
21
- const encoder = new TextEncoder();
22
- const messageUint8Array = encoder.encode(message);
23
- const signatureUint8Array = Uint8Array.from(Buffer.from(signature, 'base64'));
24
- for (let i = 0; i < secrets.length; i++) {
25
- const keyUint8Array = encoder.encode(secrets[i]);
26
- const cryptoKey = await subtle.importKey('raw', keyUint8Array, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
27
- const result = await subtle.verify('HMAC', cryptoKey, signatureUint8Array, messageUint8Array);
28
- if (result) {
29
- return message;
30
- }
31
- }
32
- return false;
33
- }
34
- /**
35
- * Creates a signed cookie value in the format `s:[value].[signature]`
36
- * @param value
37
- * @param secret
38
- */
39
- async function signCookie(value, secret) {
40
- // Convert the value and secret to Uint8Array
41
- const encoder = new TextEncoder();
42
- const messageUint8Array = encoder.encode(value);
43
- const keyUint8Array = encoder.encode(secret);
44
- // Import the secret as a CryptoKey
45
- const cryptoKey = await subtle.importKey('raw', keyUint8Array, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
46
- const signature = await subtle.sign('HMAC', cryptoKey, messageUint8Array);
47
- // Encode in base64
48
- const b64Signature = Buffer.from(signature)
49
- .toString('base64')
50
- .replace(/=+$/, '');
51
- return `s:${value}.${b64Signature}`;
52
- }
53
- function validateConfig(config) {
54
- if (!config.store) {
55
- throw new Error('[h3-session] Session store is required!');
56
- }
57
- if (!config.secret) {
58
- throw new Error('[h3-session] Session secret is required!');
59
- }
60
- }
61
- /**
62
- * Attach a session to an H3Event
63
- * @param event
64
- * @param config
65
- */
66
- export async function useSession(event, config) {
67
- // Skip if session is already attached
68
- if (event.context.session) {
69
- return;
70
- }
71
- validateConfig(config);
72
- // Populate default config values
73
- const sessionConfig = defu(config, {
74
- name: 'connect.sid',
75
- genid: (_event) => {
76
- return randomUUID();
77
- },
78
- generate: () => ({}),
79
- cookie: { path: '/', httpOnly: true, secure: true, maxAge: null },
80
- saveUninitialized: false,
81
- });
82
- const { store } = sessionConfig;
83
- const generate = async () => {
84
- return await Promise.resolve({
85
- id: sessionConfig.genid(event),
86
- data: sessionConfig.generate(),
87
- });
88
- };
89
- // Secret can be a string or array, normalize to an array
90
- const normalizedSecrets = Array.isArray(sessionConfig.secret)
91
- ? sessionConfig.secret
92
- : [sessionConfig.secret];
93
- const createSessionCookie = async (sid) => {
94
- let signedCookie;
95
- const cookie = {
96
- ...sessionConfig.cookie,
97
- // Default to a max age of one day
98
- maxAge: sessionConfig.cookie.maxAge || 60 * 60 * 24,
99
- setSessionId: async (sid) => {
100
- signedCookie = await signCookie(sid, normalizedSecrets[normalizedSecrets.length - 1]);
101
- setCookie(event, sessionConfig.name, signedCookie, cookie);
102
- },
103
- };
104
- await cookie.setSessionId(sid);
105
- // Update the Set-Cookie header whenever a property on the cookie object changes
106
- // We can't hook into the onBeforeResponse, so this is the best alternative
107
- return new Proxy(cookie, {
108
- set(target, property, value) {
109
- // @ts-expect-error Implicitly has type any
110
- target[property] = value;
111
- setCookie(event, sessionConfig.name, signedCookie, cookie);
112
- return true;
113
- },
114
- });
115
- };
116
- // Check the request for a session cookie
117
- const rawCookie = getCookie(event, sessionConfig.name);
118
- // Extract the ID from the cookie
119
- const sessionId = rawCookie
120
- ? await unsignCookie(rawCookie, normalizedSecrets)
121
- : null;
122
- // Load the session data from the store
123
- let sessionData;
124
- if (sessionId) {
125
- sessionData = await store.get(sessionId);
126
- }
127
- async function createNewSession() {
128
- const { id, data } = await generate();
129
- const cookie = await createSessionCookie(id);
130
- event.context.session = new Session(id, data, store, generate, cookie);
131
- if (sessionConfig.saveUninitialized) {
132
- await event.context.session.save();
133
- }
134
- // Set sessionId on event context
135
- event.context.sessionId = id;
136
- }
137
- async function createExistingSession(id, data) {
138
- const cookie = await createSessionCookie(id);
139
- if (store.touch) {
140
- // touch existing sessions to refresh their TTLs
141
- await store.touch(id, data);
142
- }
143
- event.context.session = new Session(id, data, store, generate, cookie);
144
- // Set sessionId on event context
145
- event.context.sessionId = id;
146
- }
147
- // Create the session data and a new ID if it does not exist
148
- if (!sessionId || !sessionData) {
149
- await createNewSession();
150
- }
151
- else {
152
- await createExistingSession(sessionId, sessionData);
153
- }
154
- // expose session store
155
- event.context.sessionStore = store;
156
- }
157
- export * from './session';
158
- export * from './unstorage-store';
159
- //# sourceMappingURL=index.js.map
package/dist/session.js DELETED
@@ -1,37 +0,0 @@
1
- export class Session {
2
- #id;
3
- #store;
4
- data;
5
- #generate;
6
- cookie;
7
- constructor(id, data, store, generate, cookie) {
8
- this.data = data;
9
- this.#id = id;
10
- this.#store = store;
11
- this.#generate = generate;
12
- this.cookie = cookie;
13
- }
14
- get id() {
15
- return this.#id;
16
- }
17
- async save() {
18
- await this.#store.set(this.#id, { ...this.data, cookie: this.cookie });
19
- }
20
- async reload() {
21
- this.data = (await this.#store.get(this.#id)) ??
22
- (await this.#generate()).data;
23
- }
24
- async destroy() {
25
- // Delete the cookie by setting maxAge to 0
26
- this.cookie.maxAge = 0;
27
- await this.#store.destroy(this.#id);
28
- }
29
- async regenerate() {
30
- await this.#store.destroy(this.#id);
31
- const { data, id } = await this.#generate();
32
- this.data = data;
33
- this.#id = id;
34
- await Promise.all([this.cookie.setSessionId(id), this.save()]);
35
- }
36
- }
37
- //# sourceMappingURL=session.js.map
@@ -1,111 +0,0 @@
1
- import { z } from 'zod';
2
- const CookieSchema = z.object({
3
- domain: z.string().optional(),
4
- expires: z.coerce.date().optional(),
5
- httpOnly: z.boolean().optional(),
6
- maxAge: z.number().optional(),
7
- path: z.string().optional(),
8
- sameSite: z.enum(['lax', 'none', 'strict']).or(z.boolean()).optional(),
9
- secure: z.boolean().optional(),
10
- });
11
- export class UnstorageSessionStore {
12
- storage;
13
- prefix;
14
- ttl;
15
- constructor(storage, options) {
16
- this.storage = storage;
17
- this.prefix = options?.prefix ?? 'sess';
18
- this.ttl = options?.ttl ?? 60 * 60 * 24;
19
- }
20
- /**
21
- * Destroy the session with the specified session ID
22
- * @param sid the un-prefixed session ID
23
- */
24
- async destroy(sid) {
25
- await this.storage.removeItem(this.getKey(sid));
26
- }
27
- /**
28
- * Get the session with the specified session ID
29
- * @param sid the un-prefixed session ID
30
- */
31
- async get(sid) {
32
- const item = await this.storage.getItem(this.getKey(sid));
33
- // If the cookie saved in the storage is not valid, delete it
34
- if (item && item.cookie) {
35
- try {
36
- item.cookie = CookieSchema.parse(item.cookie);
37
- }
38
- catch {
39
- delete item.cookie;
40
- }
41
- }
42
- return item ?? undefined;
43
- }
44
- /**
45
- * Save the session with the specified session ID
46
- * If the session has expired (has a TTL <= 0) it is deleted.
47
- * @param sid the un-prefixed session ID
48
- * @param data the session data
49
- */
50
- async set(sid, data) {
51
- const ttl = this.getTTL(data);
52
- if (ttl > 0) {
53
- await this.storage.setItem(this.getKey(sid), data, { ttl });
54
- }
55
- else {
56
- await this.destroy(sid);
57
- }
58
- }
59
- /**
60
- * Update a session's TTL
61
- * @param sid the un-prefixed session ID
62
- * @param data the session data
63
- */
64
- async touch(sid, data) {
65
- // For now, it seems the best way to bump the TTL through the unstorage
66
- // interface is by re-saving the data
67
- await this.set(sid, data);
68
- }
69
- /**
70
- * Remove all saved sessions
71
- */
72
- async clear() {
73
- return await this.storage.clear(this.prefix);
74
- }
75
- /**
76
- * Fetch all saved sessions.
77
- */
78
- async all() {
79
- const keys = await this.getAllKeys();
80
- const values = await this.storage.getItems(keys);
81
- return values.map(({ value }) => {
82
- return value;
83
- });
84
- }
85
- /**
86
- * Returns the number of saved sessions
87
- */
88
- async length() {
89
- const keys = await this.getAllKeys();
90
- return keys.length;
91
- }
92
- async getAllKeys() {
93
- return await this.storage.getKeys(this.prefix);
94
- }
95
- getKey(key) {
96
- return [this.prefix, key].join(':');
97
- }
98
- /**
99
- * Get the TTL for the session, either from the TTL function if it exists
100
- * or falling back to the default TTL value.
101
- * @param session
102
- * @private
103
- */
104
- getTTL(session) {
105
- if (typeof this.ttl === 'function') {
106
- return this.ttl(session);
107
- }
108
- return this.ttl;
109
- }
110
- }
111
- //# sourceMappingURL=unstorage-store.js.map