@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 +6 -0
- package/dist/index.d.mts +138 -0
- package/dist/index.d.ts +138 -0
- package/dist/index.mjs +276 -0
- package/package.json +13 -10
- package/dist/index.js +0 -159
- package/dist/session.js +0 -37
- package/dist/unstorage-store.js +0 -111
package/CHANGELOG.md
CHANGED
package/dist/index.d.mts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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.
|
|
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.
|
|
12
|
+
".": "./dist/index.mjs"
|
|
13
13
|
},
|
|
14
|
-
"main": "./dist/index.
|
|
14
|
+
"main": "./dist/index.mjs",
|
|
15
15
|
"files": [
|
|
16
16
|
"CHANGELOG.md",
|
|
17
|
-
"
|
|
17
|
+
"dist/**"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "
|
|
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
|
-
"@
|
|
37
|
-
"
|
|
38
|
-
"
|
|
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": "
|
|
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
|
package/dist/unstorage-store.js
DELETED
|
@@ -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
|