@scayle/h3-session 0.4.1 → 0.4.2

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,11 @@
1
1
  # @scayle/h3-session
2
2
 
3
+ ## 0.4.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Build the package with `unbuild`
8
+
3
9
  ## 0.4.1
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,138 @@
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<RawSession | 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<RawSession[]>;
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
+ cookie?: SessionCookieOptions;
89
+ };
90
+ interface SessionStore {
91
+ all?: () => Promise<RawSession[]>;
92
+ destroy: (sid: string) => Promise<void>;
93
+ clear?: () => Promise<void>;
94
+ length?: () => Promise<number>;
95
+ get: (sid: string) => Promise<RawSession | undefined>;
96
+ set: (sid: string, data: RawSession) => Promise<void>;
97
+ touch: (sid: string, data: SessionDataT) => Promise<void>;
98
+ }
99
+ interface H3SessionOptions {
100
+ store: SessionStore;
101
+ cookie?: {
102
+ domain?: string;
103
+ expires?: Date;
104
+ httpOnly?: boolean;
105
+ maxAge?: number;
106
+ path?: string;
107
+ sameSite?: true | false | 'lax' | 'none' | 'strict';
108
+ secure?: boolean;
109
+ };
110
+ name?: string;
111
+ genid?: (event: H3Event) => string;
112
+ generate?: () => SessionDataT;
113
+ saveUninitialized?: boolean;
114
+ secret: string | string[];
115
+ }
116
+ declare module 'h3' {
117
+ interface H3EventContext {
118
+ session: Session;
119
+ sessionId: string;
120
+ sessionStore: SessionStore;
121
+ }
122
+ }
123
+ /**
124
+ * Verify that a cookie was signed with one of the secrets
125
+ * If it's valid, return the embedded message
126
+ *
127
+ * @param value a cookie value in the format `s:[value].[signature]`
128
+ * @param secrets an array of secret strings to verify with
129
+ */
130
+ declare function unsignCookie(value: string, secrets: string[]): Promise<string | false>;
131
+ /**
132
+ * Attach a session to an H3Event
133
+ * @param event
134
+ * @param config
135
+ */
136
+ declare function useSession(event: H3Event, config: H3SessionOptions): Promise<void>;
137
+
138
+ export { type RawSession, Session, type SessionCookie, type SessionCookieOptions, type SessionDataT, type SessionMethods, type SessionStore, UnstorageSessionStore, unsignCookie, useSession };
@@ -0,0 +1,138 @@
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<RawSession | 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<RawSession[]>;
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
+ cookie?: SessionCookieOptions;
89
+ };
90
+ interface SessionStore {
91
+ all?: () => Promise<RawSession[]>;
92
+ destroy: (sid: string) => Promise<void>;
93
+ clear?: () => Promise<void>;
94
+ length?: () => Promise<number>;
95
+ get: (sid: string) => Promise<RawSession | undefined>;
96
+ set: (sid: string, data: RawSession) => Promise<void>;
97
+ touch: (sid: string, data: SessionDataT) => Promise<void>;
98
+ }
99
+ interface H3SessionOptions {
100
+ store: SessionStore;
101
+ cookie?: {
102
+ domain?: string;
103
+ expires?: Date;
104
+ httpOnly?: boolean;
105
+ maxAge?: number;
106
+ path?: string;
107
+ sameSite?: true | false | 'lax' | 'none' | 'strict';
108
+ secure?: boolean;
109
+ };
110
+ name?: string;
111
+ genid?: (event: H3Event) => string;
112
+ generate?: () => SessionDataT;
113
+ saveUninitialized?: boolean;
114
+ secret: string | string[];
115
+ }
116
+ declare module 'h3' {
117
+ interface H3EventContext {
118
+ session: Session;
119
+ sessionId: string;
120
+ sessionStore: SessionStore;
121
+ }
122
+ }
123
+ /**
124
+ * Verify that a cookie was signed with one of the secrets
125
+ * If it's valid, return the embedded message
126
+ *
127
+ * @param value a cookie value in the format `s:[value].[signature]`
128
+ * @param secrets an array of secret strings to verify with
129
+ */
130
+ declare function unsignCookie(value: string, secrets: string[]): Promise<string | false>;
131
+ /**
132
+ * Attach a session to an H3Event
133
+ * @param event
134
+ * @param config
135
+ */
136
+ declare function useSession(event: H3Event, config: H3SessionOptions): Promise<void>;
137
+
138
+ export { type RawSession, Session, type SessionCookie, type SessionCookieOptions, type SessionDataT, type SessionMethods, type SessionStore, UnstorageSessionStore, unsignCookie, useSession };
package/dist/index.mjs ADDED
@@ -0,0 +1,276 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import { getCookie, setCookie } from 'h3';
3
+ import { defu } from 'defu';
4
+ import { subtle, randomUUID } from 'uncrypto';
5
+ import { z } from 'zod';
6
+
7
+ class Session {
8
+ #id;
9
+ #store;
10
+ data;
11
+ #generate;
12
+ cookie;
13
+ constructor(id, data, store, generate, cookie) {
14
+ this.data = data;
15
+ this.#id = id;
16
+ this.#store = store;
17
+ this.#generate = generate;
18
+ this.cookie = cookie;
19
+ }
20
+ get id() {
21
+ return this.#id;
22
+ }
23
+ async save() {
24
+ await this.#store.set(this.#id, { ...this.data, cookie: this.cookie });
25
+ }
26
+ async reload() {
27
+ this.data = await this.#store.get(this.#id) ?? (await this.#generate()).data;
28
+ }
29
+ async destroy() {
30
+ this.cookie.maxAge = 0;
31
+ await this.#store.destroy(this.#id);
32
+ }
33
+ async regenerate() {
34
+ await this.#store.destroy(this.#id);
35
+ const { data, id } = await this.#generate();
36
+ this.data = data;
37
+ this.#id = id;
38
+ await Promise.all([this.cookie.setSessionId(id), this.save()]);
39
+ }
40
+ }
41
+
42
+ const CookieSchema = z.object({
43
+ domain: z.string().optional(),
44
+ expires: z.coerce.date().optional(),
45
+ httpOnly: z.boolean().optional(),
46
+ maxAge: z.number().optional(),
47
+ path: z.string().optional(),
48
+ sameSite: z.enum(["lax", "none", "strict"]).or(z.boolean()).optional(),
49
+ secure: z.boolean().optional()
50
+ });
51
+ class UnstorageSessionStore {
52
+ storage;
53
+ prefix;
54
+ ttl;
55
+ constructor(storage, options) {
56
+ this.storage = storage;
57
+ this.prefix = options?.prefix ?? "sess";
58
+ this.ttl = options?.ttl ?? 60 * 60 * 24;
59
+ }
60
+ /**
61
+ * Destroy the session with the specified session ID
62
+ * @param sid the un-prefixed session ID
63
+ */
64
+ async destroy(sid) {
65
+ await this.storage.removeItem(this.getKey(sid));
66
+ }
67
+ /**
68
+ * Get the session with the specified session ID
69
+ * @param sid the un-prefixed session ID
70
+ */
71
+ async get(sid) {
72
+ const item = await this.storage.getItem(this.getKey(sid));
73
+ if (item && item.cookie) {
74
+ try {
75
+ item.cookie = CookieSchema.parse(item.cookie);
76
+ } catch {
77
+ delete item.cookie;
78
+ }
79
+ }
80
+ return item ?? void 0;
81
+ }
82
+ /**
83
+ * Save the session with the specified session ID
84
+ * If the session has expired (has a TTL <= 0) it is deleted.
85
+ * @param sid the un-prefixed session ID
86
+ * @param data the session data
87
+ */
88
+ async set(sid, data) {
89
+ const ttl = this.getTTL(data);
90
+ if (ttl > 0) {
91
+ await this.storage.setItem(this.getKey(sid), data, { ttl });
92
+ } else {
93
+ await this.destroy(sid);
94
+ }
95
+ }
96
+ /**
97
+ * Update a session's TTL
98
+ * @param sid the un-prefixed session ID
99
+ * @param data the session data
100
+ */
101
+ async touch(sid, data) {
102
+ await this.set(sid, data);
103
+ }
104
+ /**
105
+ * Remove all saved sessions
106
+ */
107
+ async clear() {
108
+ return await this.storage.clear(this.prefix);
109
+ }
110
+ /**
111
+ * Fetch all saved sessions.
112
+ */
113
+ async all() {
114
+ const keys = await this.getAllKeys();
115
+ const values = await this.storage.getItems(keys);
116
+ return values.map(({ value }) => {
117
+ return value;
118
+ });
119
+ }
120
+ /**
121
+ * Returns the number of saved sessions
122
+ */
123
+ async length() {
124
+ const keys = await this.getAllKeys();
125
+ return keys.length;
126
+ }
127
+ async getAllKeys() {
128
+ return await this.storage.getKeys(this.prefix);
129
+ }
130
+ getKey(key) {
131
+ return [this.prefix, key].join(":");
132
+ }
133
+ /**
134
+ * Get the TTL for the session, either from the TTL function if it exists
135
+ * or falling back to the default TTL value.
136
+ * @param session
137
+ * @private
138
+ */
139
+ getTTL(session) {
140
+ if (typeof this.ttl === "function") {
141
+ return this.ttl(session);
142
+ }
143
+ return this.ttl;
144
+ }
145
+ }
146
+
147
+ async function unsignCookie(value, secrets) {
148
+ const matches = /s:([^.]*)\.(.*)/.exec(value);
149
+ if (!matches) {
150
+ return false;
151
+ }
152
+ const [, message, signature] = matches;
153
+ const encoder = new TextEncoder();
154
+ const messageUint8Array = encoder.encode(message);
155
+ const signatureUint8Array = Uint8Array.from(Buffer.from(signature, "base64"));
156
+ for (let i = 0; i < secrets.length; i++) {
157
+ const keyUint8Array = encoder.encode(secrets[i]);
158
+ const cryptoKey = await subtle.importKey(
159
+ "raw",
160
+ keyUint8Array,
161
+ { name: "HMAC", hash: "SHA-256" },
162
+ false,
163
+ ["verify"]
164
+ );
165
+ const result = await subtle.verify(
166
+ "HMAC",
167
+ cryptoKey,
168
+ signatureUint8Array,
169
+ messageUint8Array
170
+ );
171
+ if (result) {
172
+ return message;
173
+ }
174
+ }
175
+ return false;
176
+ }
177
+ async function signCookie(value, secret) {
178
+ const encoder = new TextEncoder();
179
+ const messageUint8Array = encoder.encode(value);
180
+ const keyUint8Array = encoder.encode(secret);
181
+ const cryptoKey = await subtle.importKey(
182
+ "raw",
183
+ keyUint8Array,
184
+ { name: "HMAC", hash: "SHA-256" },
185
+ false,
186
+ ["sign"]
187
+ );
188
+ const signature = await subtle.sign("HMAC", cryptoKey, messageUint8Array);
189
+ const b64Signature = Buffer.from(signature).toString("base64").replace(/=+$/, "");
190
+ return `s:${value}.${b64Signature}`;
191
+ }
192
+ function validateConfig(config) {
193
+ if (!config.store) {
194
+ throw new Error("[h3-session] Session store is required!");
195
+ }
196
+ if (!config.secret) {
197
+ throw new Error("[h3-session] Session secret is required!");
198
+ }
199
+ }
200
+ async function useSession(event, config) {
201
+ if (event.context.session) {
202
+ return;
203
+ }
204
+ validateConfig(config);
205
+ const sessionConfig = defu(config, {
206
+ name: "connect.sid",
207
+ genid: (_event) => {
208
+ return randomUUID();
209
+ },
210
+ generate: () => ({}),
211
+ cookie: { path: "/", httpOnly: true, secure: true, maxAge: null },
212
+ saveUninitialized: false
213
+ });
214
+ const { store } = sessionConfig;
215
+ const generate = async () => {
216
+ return await Promise.resolve({
217
+ id: sessionConfig.genid(event),
218
+ data: sessionConfig.generate()
219
+ });
220
+ };
221
+ const normalizedSecrets = Array.isArray(sessionConfig.secret) ? sessionConfig.secret : [sessionConfig.secret];
222
+ const createSessionCookie = async (sid) => {
223
+ let signedCookie;
224
+ const cookie = {
225
+ ...sessionConfig.cookie,
226
+ // Default to a max age of one day
227
+ maxAge: sessionConfig.cookie.maxAge || 60 * 60 * 24,
228
+ setSessionId: async (sid2) => {
229
+ signedCookie = await signCookie(
230
+ sid2,
231
+ normalizedSecrets[normalizedSecrets.length - 1]
232
+ );
233
+ setCookie(event, sessionConfig.name, signedCookie, cookie);
234
+ }
235
+ };
236
+ await cookie.setSessionId(sid);
237
+ return new Proxy(cookie, {
238
+ set(target, property, value) {
239
+ target[property] = value;
240
+ setCookie(event, sessionConfig.name, signedCookie, cookie);
241
+ return true;
242
+ }
243
+ });
244
+ };
245
+ const rawCookie = getCookie(event, sessionConfig.name);
246
+ const sessionId = rawCookie ? await unsignCookie(rawCookie, normalizedSecrets) : null;
247
+ let sessionData;
248
+ if (sessionId) {
249
+ sessionData = await store.get(sessionId);
250
+ }
251
+ async function createNewSession() {
252
+ const { id, data } = await generate();
253
+ const cookie = await createSessionCookie(id);
254
+ event.context.session = new Session(id, data, store, generate, cookie);
255
+ if (sessionConfig.saveUninitialized) {
256
+ await event.context.session.save();
257
+ }
258
+ event.context.sessionId = id;
259
+ }
260
+ async function createExistingSession(id, data) {
261
+ const cookie = await createSessionCookie(id);
262
+ if (store.touch) {
263
+ await store.touch(id, data);
264
+ }
265
+ event.context.session = new Session(id, data, store, generate, cookie);
266
+ event.context.sessionId = id;
267
+ }
268
+ if (!sessionId || !sessionData) {
269
+ await createNewSession();
270
+ } else {
271
+ await createExistingSession(sessionId, sessionData);
272
+ }
273
+ event.context.sessionStore = store;
274
+ }
275
+
276
+ export { Session, UnstorageSessionStore, unsignCookie, useSession };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scayle/h3-session",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Persistent sessions for h3",
5
5
  "author": "SCAYLE Commerce Engine",
6
6
  "license": "MIT",
@@ -9,15 +9,16 @@
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",
@@ -33,14 +34,16 @@
33
34
  "zod": "^3.23.8"
34
35
  },
35
36
  "devDependencies": {
36
- "@scayle/eslint-config-storefront": "4.3.1",
37
- "dprint": "0.47.2",
38
- "eslint": "9.12.0",
37
+ "@arethetypeswrong/cli": "0.17.1",
38
+ "@scayle/eslint-config-storefront": "4.4.0",
39
+ "dprint": "0.47.6",
40
+ "eslint": "9.17.0",
39
41
  "eslint-formatter-gitlab": "5.1.0",
40
42
  "h3": "1.13.0",
41
- "typescript": "5.6.3"
43
+ "typescript": "5.6.3",
44
+ "unbuild": "3.0.1"
42
45
  },
43
46
  "volta": {
44
- "node": "20.18.0"
47
+ "node": "22.12.0"
45
48
  }
46
49
  }
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