@scayle/h3-session 0.3.4
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 +47 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/dist/index.js +158 -0
- package/dist/session.js +37 -0
- package/dist/unstorage-store.js +111 -0
- package/package.json +48 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @scayle/h3-session
|
|
2
|
+
|
|
3
|
+
## 0.3.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Add license file
|
|
8
|
+
|
|
9
|
+
## 0.3.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- New cookie settings should apply to existing cookies
|
|
14
|
+
|
|
15
|
+
## 0.3.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- Fix loading sessions created by express-session
|
|
20
|
+
|
|
21
|
+
## 0.3.1
|
|
22
|
+
|
|
23
|
+
### Patch Changes
|
|
24
|
+
|
|
25
|
+
- raise required h3 peerDependency version to v1.8.0 and use only fixed dependencies
|
|
26
|
+
|
|
27
|
+
## 0.3.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- Support typing the session data
|
|
32
|
+
|
|
33
|
+
### Patch Changes
|
|
34
|
+
|
|
35
|
+
- Remove unused proxy option
|
|
36
|
+
|
|
37
|
+
## 0.2.1
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- Bump the compilation target to ES2022
|
|
42
|
+
|
|
43
|
+
## 0.2.0
|
|
44
|
+
|
|
45
|
+
### Minor Changes
|
|
46
|
+
|
|
47
|
+
- Add h3-session package
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 SCAYLE GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# @scayle/h3-session
|
|
2
|
+
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![npm downloads][npm-downloads-src]][npm-downloads-href]
|
|
5
|
+
[![License][license-src]][license-href]
|
|
6
|
+
|
|
7
|
+
Persistent sessions for [h3](https://github.com/unjs/h3). Based on [express-session](https://www.npmjs.com/package/express-session).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Using pnpm
|
|
13
|
+
pnpm add @scayle/h3-session
|
|
14
|
+
|
|
15
|
+
# Using yarn
|
|
16
|
+
yarn add @scayle/h3-session
|
|
17
|
+
|
|
18
|
+
# Using npm
|
|
19
|
+
npm install @scayle/h3-session
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { useSession, UnstorageSessionStore } from '@scayle/h3-session'
|
|
26
|
+
import { createStorage } from 'unstorage'
|
|
27
|
+
|
|
28
|
+
const DEFAULT_TTL = 60 * 60 * 24
|
|
29
|
+
|
|
30
|
+
const redisStore = UnstorageSessionStore(
|
|
31
|
+
createStorage({
|
|
32
|
+
driver: redisDriver({
|
|
33
|
+
host: 'localhost',
|
|
34
|
+
port: 6379,
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
{
|
|
38
|
+
ttl: DEFAULT_TTL,
|
|
39
|
+
},
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
export default defineEventHandler(async (event) => {
|
|
43
|
+
await useSession(event, {
|
|
44
|
+
store: redisStore,
|
|
45
|
+
secret: 'my-secret',
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const sessionId = event.context.sessionId
|
|
49
|
+
const sessionData = event.context.session.data
|
|
50
|
+
})
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## useSession options
|
|
54
|
+
|
|
55
|
+
The following options can be passed as the second argument to `useSession`. `store` and `secret` must be defined.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
interface H3SessionOptions {
|
|
59
|
+
// Where session data will be stored
|
|
60
|
+
store: SessionStore
|
|
61
|
+
|
|
62
|
+
// Settings for configuring the session cookie
|
|
63
|
+
// Cookies are serialized with [`cookie-es`](https://github.com/unjs/cookie-es).
|
|
64
|
+
// The default value is { path: '/', httpOnly: true, secure: true, maxAge: null }.
|
|
65
|
+
cookie?: {
|
|
66
|
+
domain?: string
|
|
67
|
+
expires?: Date
|
|
68
|
+
httpOnly?: boolean
|
|
69
|
+
maxAge?: number
|
|
70
|
+
path?: string
|
|
71
|
+
sameSite?: true | false | 'lax' | 'none' | 'strict'
|
|
72
|
+
secure?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// The name of the session cookie. Defaults to 'connect.sid'
|
|
76
|
+
name?: string
|
|
77
|
+
|
|
78
|
+
// Function to generate a session ID. Defaults to randomUUID
|
|
79
|
+
genid?: (event: H3Event) => string
|
|
80
|
+
|
|
81
|
+
// Function to generate new session data
|
|
82
|
+
generate?: () => SessionDataT
|
|
83
|
+
|
|
84
|
+
// Should the session be automatically saved on initialization?
|
|
85
|
+
saveUninitialized?: boolean
|
|
86
|
+
|
|
87
|
+
// Secret(s) to use for cookie signing
|
|
88
|
+
secret: string | string[]
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Session object
|
|
93
|
+
|
|
94
|
+
`useSession` attaches a `Session` object to `event.context.session`. The `Session` object has several methods along with an `id` property containing the session's ID, a `data` property which contains the session's data and a `cookie` property representing the session's cookie.
|
|
95
|
+
|
|
96
|
+
### Session.save()
|
|
97
|
+
|
|
98
|
+
Save the session to the store. Unlike `express-session`, session data is not saved automatically when it is changed, so this function should be called after modifying the session or at the end of the request.
|
|
99
|
+
|
|
100
|
+
### Session.reload()
|
|
101
|
+
|
|
102
|
+
Reload the session's data from the store.
|
|
103
|
+
|
|
104
|
+
### Session.destroy()
|
|
105
|
+
|
|
106
|
+
Destroy the session by removing its data from the store and deleting the cookie.
|
|
107
|
+
|
|
108
|
+
### Session.regenerate()
|
|
109
|
+
|
|
110
|
+
Recreate the session with a new ID and empty data.
|
|
111
|
+
|
|
112
|
+
## SessionCookie
|
|
113
|
+
|
|
114
|
+
The `cookie` property on the session can be used to control the session's cookie. Any of the cookie serialization options such as `maxAge` or `domain` can be edited and will affect the `Set-Cookie` header sent in the response. Additionally, `setSessionId(sid: string)` can be called to update the cookie's value with a new session ID.
|
|
115
|
+
|
|
116
|
+
## SessionStore
|
|
117
|
+
|
|
118
|
+
Any object implementing the `SessionStore` interface can be used as the `store` option.
|
|
119
|
+
|
|
120
|
+
There is an included `UnstorageSessionStore` class to create `SessionStore` backed by an [`unstorage`](https://unstorage.unjs.io/) provider. To avoid accumulating dead sessions, use a driver (such as redis) which supports the `ttl` option.
|
|
121
|
+
|
|
122
|
+
A reference to the store is added to `event.context.sessionStore` to enable manual management of the session data. (e.g. clearing all saved sessions)
|
|
123
|
+
|
|
124
|
+
## Cookie signing
|
|
125
|
+
|
|
126
|
+
The value of the session cookie is a signed variant of the session's ID. The format is `s:[sessionID].[signature]`. e.g. `s:7247d6e9-8ddb-4005-988b-9aa82bd7d6d5.lERU16bdv6tojUNeM8V2UOARyNxHNaWKIYi4bLRxx7o`. This matches the format used by `express-session`, so existing sessions can be preserved when migrating.
|
|
127
|
+
|
|
128
|
+
The secret used for signing cookies is set in the options of `useSession`. It can be a `string` or `string[]`. When specified as an array, the last item will be used for signing new session cookies, but all secrets will be used to verify existing cookies. This allows changing the signing secret without invalidating existing sessions.
|
|
129
|
+
|
|
130
|
+
## Typing
|
|
131
|
+
|
|
132
|
+
The type of the session data can be specified by augmenting the `SessionDataT` interface.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
declare module '@scayle/h3-session' {
|
|
136
|
+
export interface SessionDataT {
|
|
137
|
+
userId: string
|
|
138
|
+
token: string
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
<!-- Badges -->
|
|
144
|
+
|
|
145
|
+
[npm-version-src]: https://img.shields.io/npm/v/@scayle/h3-session/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
146
|
+
[npm-version-href]: https://npmjs.com/package/@scayle/h3-session
|
|
147
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/@scayle/h3-session.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
148
|
+
[npm-downloads-href]: https://npmjs.com/package/@scayle/h3-session
|
|
149
|
+
[license-src]: https://img.shields.io/npm/l/@scayle/h3-session.svg?style=flat&colorA=18181B&colorB=28CF8D
|
|
150
|
+
[license-href]: https://npmjs.com/package/@scayle/h3-session
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// A session implementation for h3 based on express-session
|
|
2
|
+
import { getCookie, setCookie } from 'h3';
|
|
3
|
+
import { defu } from 'defu';
|
|
4
|
+
import { subtle, randomUUID } from 'uncrypto';
|
|
5
|
+
import { Session } from './session';
|
|
6
|
+
/**
|
|
7
|
+
* Verify that a cookie was signed with one of the secrets
|
|
8
|
+
* If it's valid, return the embedded message
|
|
9
|
+
*
|
|
10
|
+
* @param value a cookie value in the format `s:[value].[signature]`
|
|
11
|
+
* @param secrets an array of secret strings to verify with
|
|
12
|
+
*/
|
|
13
|
+
async function unsignCookie(value, secrets) {
|
|
14
|
+
// Validate cookie format
|
|
15
|
+
const matches = /s:([^.]*)\.(.*)/.exec(value);
|
|
16
|
+
if (!matches) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
const [, message, signature] = matches;
|
|
20
|
+
const encoder = new TextEncoder();
|
|
21
|
+
const messageUint8Array = encoder.encode(message);
|
|
22
|
+
const signatureUint8Array = Uint8Array.from(Buffer.from(signature, 'base64'));
|
|
23
|
+
for (let i = 0; i < secrets.length; i++) {
|
|
24
|
+
const keyUint8Array = encoder.encode(secrets[i]);
|
|
25
|
+
const cryptoKey = await subtle.importKey('raw', keyUint8Array, { name: 'HMAC', hash: 'SHA-256' }, false, ['verify']);
|
|
26
|
+
const result = await subtle.verify('HMAC', cryptoKey, signatureUint8Array, messageUint8Array);
|
|
27
|
+
if (result) {
|
|
28
|
+
return message;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Creates a signed cookie value in the format `s:[value].[signature]`
|
|
35
|
+
* @param value
|
|
36
|
+
* @param secret
|
|
37
|
+
*/
|
|
38
|
+
async function signCookie(value, secret) {
|
|
39
|
+
// Convert the value and secret to Uint8Array
|
|
40
|
+
const encoder = new TextEncoder();
|
|
41
|
+
const messageUint8Array = encoder.encode(value);
|
|
42
|
+
const keyUint8Array = encoder.encode(secret);
|
|
43
|
+
// Import the secret as a CryptoKey
|
|
44
|
+
const cryptoKey = await subtle.importKey('raw', keyUint8Array, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
45
|
+
const signature = await subtle.sign('HMAC', cryptoKey, messageUint8Array);
|
|
46
|
+
// Encode in base64
|
|
47
|
+
const b64Signature = Buffer.from(signature)
|
|
48
|
+
.toString('base64')
|
|
49
|
+
.replace(/=+$/, '');
|
|
50
|
+
return `s:${value}.${b64Signature}`;
|
|
51
|
+
}
|
|
52
|
+
function validateConfig(config) {
|
|
53
|
+
if (!config.store) {
|
|
54
|
+
throw new Error('[h3-session] Session store is required!');
|
|
55
|
+
}
|
|
56
|
+
if (!config.secret) {
|
|
57
|
+
throw new Error('[h3-session] Session secret is required!');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Attach a session to an H3Event
|
|
62
|
+
* @param event
|
|
63
|
+
* @param config
|
|
64
|
+
*/
|
|
65
|
+
export async function useSession(event, config) {
|
|
66
|
+
// Skip if session is already attached
|
|
67
|
+
if (event.context.session) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
validateConfig(config);
|
|
71
|
+
// Populate default config values
|
|
72
|
+
const sessionConfig = defu(config, {
|
|
73
|
+
name: 'connect.sid',
|
|
74
|
+
genid: (_event) => {
|
|
75
|
+
return randomUUID();
|
|
76
|
+
},
|
|
77
|
+
generate: () => ({}),
|
|
78
|
+
cookie: { path: '/', httpOnly: true, secure: true, maxAge: null },
|
|
79
|
+
saveUninitialized: false,
|
|
80
|
+
});
|
|
81
|
+
const { store } = sessionConfig;
|
|
82
|
+
const generate = async () => {
|
|
83
|
+
return await Promise.resolve({
|
|
84
|
+
id: sessionConfig.genid(event),
|
|
85
|
+
data: sessionConfig.generate(),
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
const createSessionCookie = async (sid) => {
|
|
89
|
+
let signedCookie;
|
|
90
|
+
const cookie = {
|
|
91
|
+
...sessionConfig.cookie,
|
|
92
|
+
// Default to a max age of one day
|
|
93
|
+
maxAge: sessionConfig.cookie.maxAge || 60 * 60 * 24,
|
|
94
|
+
setSessionId: async (sid) => {
|
|
95
|
+
signedCookie = await signCookie(sid, normalizedSecrets[normalizedSecrets.length - 1]);
|
|
96
|
+
setCookie(event, sessionConfig.name, signedCookie, cookie);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
await cookie.setSessionId(sid);
|
|
100
|
+
// Update the Set-Cookie header whenever a property on the cookie object changes
|
|
101
|
+
// We can't hook into the onBeforeResponse, so this is the best alternative
|
|
102
|
+
return new Proxy(cookie, {
|
|
103
|
+
set(target, property, value) {
|
|
104
|
+
// @ts-ignore
|
|
105
|
+
target[property] = value;
|
|
106
|
+
setCookie(event, sessionConfig.name, signedCookie, cookie);
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
// Secret can be a string or array, normalize to an array
|
|
112
|
+
const normalizedSecrets = Array.isArray(sessionConfig.secret)
|
|
113
|
+
? sessionConfig.secret
|
|
114
|
+
: [sessionConfig.secret];
|
|
115
|
+
// Check the request for a session cookie
|
|
116
|
+
const rawCookie = getCookie(event, sessionConfig.name);
|
|
117
|
+
// Extract the ID from the cookie
|
|
118
|
+
const sessionId = rawCookie
|
|
119
|
+
? await unsignCookie(rawCookie, normalizedSecrets)
|
|
120
|
+
: null;
|
|
121
|
+
// Load the session data from the store
|
|
122
|
+
let sessionData;
|
|
123
|
+
if (sessionId) {
|
|
124
|
+
sessionData = await store.get(sessionId);
|
|
125
|
+
}
|
|
126
|
+
async function createNewSession() {
|
|
127
|
+
const { id, data } = await generate();
|
|
128
|
+
const cookie = await createSessionCookie(id);
|
|
129
|
+
event.context.session = new Session(id, data, store, generate, cookie);
|
|
130
|
+
if (sessionConfig.saveUninitialized) {
|
|
131
|
+
await event.context.session.save();
|
|
132
|
+
}
|
|
133
|
+
// Set sessionId on event context
|
|
134
|
+
event.context.sessionId = id;
|
|
135
|
+
}
|
|
136
|
+
async function createExistingSession(id, data) {
|
|
137
|
+
const cookie = await createSessionCookie(id);
|
|
138
|
+
if (store.touch) {
|
|
139
|
+
// touch existing sessions to refresh their TTLs
|
|
140
|
+
await store.touch(id, data);
|
|
141
|
+
}
|
|
142
|
+
event.context.session = new Session(id, data, store, generate, cookie);
|
|
143
|
+
// Set sessionId on event context
|
|
144
|
+
event.context.sessionId = id;
|
|
145
|
+
}
|
|
146
|
+
// Create the session data and a new ID if it does not exist
|
|
147
|
+
if (!sessionId || !sessionData) {
|
|
148
|
+
await createNewSession();
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await createExistingSession(sessionId, sessionData);
|
|
152
|
+
}
|
|
153
|
+
// expose session store
|
|
154
|
+
event.context.sessionStore = store;
|
|
155
|
+
}
|
|
156
|
+
export * from './session';
|
|
157
|
+
export * from './unstorage-store';
|
|
158
|
+
//# sourceMappingURL=index.js.map
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
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 =
|
|
22
|
+
(await this.#store.get(this.#id)) ?? (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
|
|
@@ -0,0 +1,111 @@
|
|
|
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 (e) {
|
|
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
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@scayle/h3-session",
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"description": "Persistent sessions for h3",
|
|
5
|
+
"author": "SCAYLE Commerce Engine",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"registry": "https://registry.npmjs.org/"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"./dist/*.js"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "yarn tsc -p tsconfig.json",
|
|
21
|
+
"format": "prettier --ignore-unknown --log-level warn --check .",
|
|
22
|
+
"format:fix": "prettier --ignore-unknown --log-level warn --write .",
|
|
23
|
+
"lint": "eslint . --format gitlab",
|
|
24
|
+
"lint:fix": "eslint . --fix"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"h3": "^1.8.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"defu": "6.1.3",
|
|
31
|
+
"uncrypto": "0.1.3",
|
|
32
|
+
"unstorage": "1.9.0",
|
|
33
|
+
"zod": "3.22.4"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@changesets/types": "5.2.1",
|
|
37
|
+
"@scayle/eslint-config-storefront": "3.2.5",
|
|
38
|
+
"@scayle/prettier-config-storefront": "2.0.2",
|
|
39
|
+
"eslint": "8.52.0",
|
|
40
|
+
"eslint-formatter-gitlab": "5.0.0",
|
|
41
|
+
"h3": "1.8.2",
|
|
42
|
+
"prettier": "3.0.0",
|
|
43
|
+
"typescript": "5.2.2"
|
|
44
|
+
},
|
|
45
|
+
"volta": {
|
|
46
|
+
"node": "20.9.0"
|
|
47
|
+
}
|
|
48
|
+
}
|