@maestro-js/session 1.0.0-alpha.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/dist/index.d.ts +98 -0
- package/dist/index.js +267 -0
- package/package.json +36 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Crypt } from '@maestro-js/crypt';
|
|
2
|
+
|
|
3
|
+
declare function cookieSessionDriver({ crypt, cookie }: {
|
|
4
|
+
crypt: Crypt.CryptService;
|
|
5
|
+
cookie?: Session.Provider.CookieOptions;
|
|
6
|
+
}): Session.Driver;
|
|
7
|
+
|
|
8
|
+
interface StoreBackend {
|
|
9
|
+
get(sessionId: string): Promise<string | null>;
|
|
10
|
+
set(sessionId: string, data: string, ttl: number | null): Promise<void>;
|
|
11
|
+
del(sessionId: string): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
declare function storeSessionDriver({ store, crypt, cookie }: {
|
|
14
|
+
store: StoreBackend;
|
|
15
|
+
crypt: Crypt.CryptService;
|
|
16
|
+
cookie?: Session.Provider.CookieOptions;
|
|
17
|
+
}): Session.Driver;
|
|
18
|
+
|
|
19
|
+
declare function create({ driver }: Session.Provider.SessionServiceConfig): Session.SessionService;
|
|
20
|
+
declare function createScope<Data extends Session.SessionData>(scopeKey: string): (session: Session) => Session.Scope<Data>;
|
|
21
|
+
declare function provider(key: Session.Provider.Key): {
|
|
22
|
+
getSession: (cookieHeader: string | null | undefined) => Promise<Session.Session>;
|
|
23
|
+
};
|
|
24
|
+
type SessionType = Session;
|
|
25
|
+
type KeysWithFallback = keyof Session.Provider.Keys extends never ? {
|
|
26
|
+
default: unknown;
|
|
27
|
+
} : Session.Provider.Keys;
|
|
28
|
+
declare const Session: {
|
|
29
|
+
provider: typeof provider;
|
|
30
|
+
createScope: typeof createScope;
|
|
31
|
+
drivers: {
|
|
32
|
+
cookie: typeof cookieSessionDriver;
|
|
33
|
+
store: typeof storeSessionDriver;
|
|
34
|
+
};
|
|
35
|
+
getSession: (cookieHeader: string | null | undefined) => Promise<Session.Session>;
|
|
36
|
+
Provider: {
|
|
37
|
+
create: typeof create;
|
|
38
|
+
register: (name: Session.Provider.Key, item: Session.SessionService) => void;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
type Session = {
|
|
42
|
+
has(key: string): boolean;
|
|
43
|
+
get(key: string): unknown | null;
|
|
44
|
+
getCsrfToken(): string;
|
|
45
|
+
all(): Session.SessionData;
|
|
46
|
+
set(key: string, value: any): void;
|
|
47
|
+
del(key: string): void;
|
|
48
|
+
flash(key: string, value: any): void;
|
|
49
|
+
reflash(): void;
|
|
50
|
+
flush(): void;
|
|
51
|
+
regenerate(): void;
|
|
52
|
+
invalidate(): void;
|
|
53
|
+
commit(): Promise<string>;
|
|
54
|
+
};
|
|
55
|
+
declare namespace Session {
|
|
56
|
+
interface SessionData {
|
|
57
|
+
[name: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
type Session = SessionType;
|
|
60
|
+
interface SessionService {
|
|
61
|
+
getSession(cookieHeader: string | null | undefined): Promise<Session>;
|
|
62
|
+
}
|
|
63
|
+
interface Scope<Data extends SessionData> {
|
|
64
|
+
has(name: keyof Data & string): boolean;
|
|
65
|
+
get<Key extends keyof Data & string>(name: Key): (Key extends keyof Data ? Data[Key] : undefined) | undefined;
|
|
66
|
+
all(): Data;
|
|
67
|
+
set<Key extends keyof Data & string>(name: Key, value: Data[Key]): void;
|
|
68
|
+
del(name: keyof Data & string): void;
|
|
69
|
+
flush(): void;
|
|
70
|
+
}
|
|
71
|
+
interface Driver {
|
|
72
|
+
read(cookieHeader: string): Promise<{
|
|
73
|
+
sessionId: string;
|
|
74
|
+
data: string;
|
|
75
|
+
} | null>;
|
|
76
|
+
write(sessionId: string, data: string): Promise<string>;
|
|
77
|
+
del(sessionId: string): Promise<void>;
|
|
78
|
+
}
|
|
79
|
+
namespace Provider {
|
|
80
|
+
interface SessionServiceConfig {
|
|
81
|
+
driver: Driver;
|
|
82
|
+
}
|
|
83
|
+
interface CookieOptions {
|
|
84
|
+
name?: string;
|
|
85
|
+
secure?: boolean;
|
|
86
|
+
httpOnly?: boolean;
|
|
87
|
+
sameSite?: 'Strict' | 'Lax' | 'None';
|
|
88
|
+
path?: string;
|
|
89
|
+
domain?: string;
|
|
90
|
+
maxAge?: number;
|
|
91
|
+
}
|
|
92
|
+
type Key = keyof KeysWithFallback;
|
|
93
|
+
interface Keys {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { Session };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3
|
+
import { ServiceRegistry } from "@maestro-js/service-registry";
|
|
4
|
+
|
|
5
|
+
// src/cookie-session-driver.ts
|
|
6
|
+
import { randomUUID } from "crypto";
|
|
7
|
+
|
|
8
|
+
// src/cookie-utils.ts
|
|
9
|
+
function parseCookies(cookieHeader) {
|
|
10
|
+
const cookies = {};
|
|
11
|
+
for (const pair of cookieHeader.split(";")) {
|
|
12
|
+
const index = pair.indexOf("=");
|
|
13
|
+
if (index === -1) continue;
|
|
14
|
+
const name = pair.slice(0, index).trim();
|
|
15
|
+
const value = pair.slice(index + 1).trim();
|
|
16
|
+
if (name) cookies[name] = decodeURIComponent(value);
|
|
17
|
+
}
|
|
18
|
+
return cookies;
|
|
19
|
+
}
|
|
20
|
+
function buildSetCookieHeader(name, value, options) {
|
|
21
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
22
|
+
parts.push(`Path=${options.path}`);
|
|
23
|
+
if (options.maxAge !== void 0) parts.push(`Max-Age=${options.maxAge}`);
|
|
24
|
+
if (options.domain) parts.push(`Domain=${options.domain}`);
|
|
25
|
+
if (options.httpOnly) parts.push("HttpOnly");
|
|
26
|
+
if (options.secure) parts.push("Secure");
|
|
27
|
+
parts.push(`SameSite=${options.sameSite}`);
|
|
28
|
+
return parts.join("; ");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// src/cookie-session-driver.ts
|
|
32
|
+
var MAX_COOKIE_BYTES = 4096;
|
|
33
|
+
function cookieSessionDriver({
|
|
34
|
+
crypt,
|
|
35
|
+
cookie
|
|
36
|
+
}) {
|
|
37
|
+
const cookieOptions = resolveCookieOptions(cookie);
|
|
38
|
+
return {
|
|
39
|
+
async read(cookieHeader) {
|
|
40
|
+
const cookies = parseCookies(cookieHeader);
|
|
41
|
+
const raw = cookies[cookieOptions.name];
|
|
42
|
+
if (!raw) return null;
|
|
43
|
+
try {
|
|
44
|
+
const data = crypt.decryptString(raw);
|
|
45
|
+
return { sessionId: randomUUID(), data };
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
async write(_sessionId, data) {
|
|
51
|
+
const encrypted = crypt.encryptString(data);
|
|
52
|
+
const byteLength = new TextEncoder().encode(encrypted).byteLength;
|
|
53
|
+
if (byteLength > MAX_COOKIE_BYTES) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Cookie session data is ${byteLength} bytes, which exceeds the ${MAX_COOKIE_BYTES}-byte cookie limit. Reduce the amount of data stored in the session or switch to a server-side session driver.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return buildSetCookieHeader(cookieOptions.name, encrypted, cookieOptions);
|
|
59
|
+
},
|
|
60
|
+
async del() {
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function resolveCookieOptions(cookie) {
|
|
65
|
+
return {
|
|
66
|
+
name: cookie?.name ?? "__session",
|
|
67
|
+
secure: cookie?.secure ?? true,
|
|
68
|
+
httpOnly: cookie?.httpOnly ?? true,
|
|
69
|
+
sameSite: cookie?.sameSite ?? "Lax",
|
|
70
|
+
path: cookie?.path ?? "/",
|
|
71
|
+
domain: cookie?.domain,
|
|
72
|
+
maxAge: cookie?.maxAge
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/store-session-driver.ts
|
|
77
|
+
function storeSessionDriver({
|
|
78
|
+
store,
|
|
79
|
+
crypt,
|
|
80
|
+
cookie
|
|
81
|
+
}) {
|
|
82
|
+
const cookieOptions = resolveCookieOptions2(cookie);
|
|
83
|
+
return {
|
|
84
|
+
async read(cookieHeader) {
|
|
85
|
+
const cookies = parseCookies(cookieHeader);
|
|
86
|
+
const raw = cookies[cookieOptions.name];
|
|
87
|
+
if (!raw) return null;
|
|
88
|
+
try {
|
|
89
|
+
const sessionId = crypt.decryptString(raw);
|
|
90
|
+
const data = await store.get(sessionId);
|
|
91
|
+
if (!data) return null;
|
|
92
|
+
return { sessionId, data };
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
async write(sessionId, data) {
|
|
98
|
+
await store.set(sessionId, data, cookieOptions.maxAge ?? null);
|
|
99
|
+
const cookieValue = crypt.encryptString(sessionId);
|
|
100
|
+
return buildSetCookieHeader(cookieOptions.name, cookieValue, cookieOptions);
|
|
101
|
+
},
|
|
102
|
+
async del(sessionId) {
|
|
103
|
+
await store.del(sessionId);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function resolveCookieOptions2(cookie) {
|
|
108
|
+
return {
|
|
109
|
+
name: cookie?.name ?? "__session",
|
|
110
|
+
secure: cookie?.secure ?? true,
|
|
111
|
+
httpOnly: cookie?.httpOnly ?? true,
|
|
112
|
+
sameSite: cookie?.sameSite ?? "Lax",
|
|
113
|
+
path: cookie?.path ?? "/",
|
|
114
|
+
domain: cookie?.domain,
|
|
115
|
+
maxAge: cookie?.maxAge
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/index.ts
|
|
120
|
+
function create({ driver }) {
|
|
121
|
+
const flashKey = `__flash`;
|
|
122
|
+
const crossSiteRequestForgeryTokenKey = `__csrf_token`;
|
|
123
|
+
async function getSession(cookieHeader) {
|
|
124
|
+
let sessionId = randomUUID2();
|
|
125
|
+
let rawData = {};
|
|
126
|
+
if (cookieHeader) {
|
|
127
|
+
try {
|
|
128
|
+
const result = await driver.read(cookieHeader);
|
|
129
|
+
if (result) {
|
|
130
|
+
sessionId = result.sessionId;
|
|
131
|
+
rawData = JSON.parse(result.data);
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
sessionId = randomUUID2();
|
|
135
|
+
rawData = {};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const oldFlashKeys = new Set(Array.isArray(rawData[flashKey]) ? rawData[flashKey] : []);
|
|
139
|
+
const newFlashKeys = /* @__PURE__ */ new Set();
|
|
140
|
+
const csrfToken = rawData[crossSiteRequestForgeryTokenKey] || randomUUID2();
|
|
141
|
+
delete rawData[flashKey];
|
|
142
|
+
delete rawData[crossSiteRequestForgeryTokenKey];
|
|
143
|
+
const meta = { sessionId, data: rawData, oldFlashKeys, newFlashKeys, regenerate: false, csrfToken };
|
|
144
|
+
function has(key) {
|
|
145
|
+
return key in meta.data;
|
|
146
|
+
}
|
|
147
|
+
function get(key) {
|
|
148
|
+
return meta.data[key];
|
|
149
|
+
}
|
|
150
|
+
function all() {
|
|
151
|
+
return { ...meta.data };
|
|
152
|
+
}
|
|
153
|
+
function set(key, value) {
|
|
154
|
+
meta.data[key] = value;
|
|
155
|
+
}
|
|
156
|
+
function flash(key, value) {
|
|
157
|
+
meta.data[key] = value;
|
|
158
|
+
meta.newFlashKeys.add(key);
|
|
159
|
+
}
|
|
160
|
+
function reflash() {
|
|
161
|
+
for (const key of meta.oldFlashKeys) {
|
|
162
|
+
meta.newFlashKeys.add(key);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function del(key) {
|
|
166
|
+
delete meta.data[key];
|
|
167
|
+
meta.oldFlashKeys.delete(key);
|
|
168
|
+
meta.newFlashKeys.delete(key);
|
|
169
|
+
}
|
|
170
|
+
function flush() {
|
|
171
|
+
meta.data = {};
|
|
172
|
+
meta.oldFlashKeys.clear();
|
|
173
|
+
meta.newFlashKeys.clear();
|
|
174
|
+
}
|
|
175
|
+
function regenerate() {
|
|
176
|
+
meta.regenerate = true;
|
|
177
|
+
}
|
|
178
|
+
function invalidate() {
|
|
179
|
+
meta.regenerate = true;
|
|
180
|
+
flush();
|
|
181
|
+
}
|
|
182
|
+
function getCsrfToken() {
|
|
183
|
+
return csrfToken;
|
|
184
|
+
}
|
|
185
|
+
async function commit() {
|
|
186
|
+
const nextSessionId = meta.regenerate ? randomUUID2() : meta.sessionId;
|
|
187
|
+
const nextCsrfToken = meta.regenerate ? randomUUID2() : meta.csrfToken;
|
|
188
|
+
for (const key of meta.oldFlashKeys) {
|
|
189
|
+
if (!meta.newFlashKeys.has(key)) {
|
|
190
|
+
delete meta.data[key];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (meta.newFlashKeys.size > 0) {
|
|
194
|
+
meta.data[flashKey] = Array.from(meta.newFlashKeys);
|
|
195
|
+
}
|
|
196
|
+
meta.data[crossSiteRequestForgeryTokenKey] = nextCsrfToken;
|
|
197
|
+
if (meta.regenerate) {
|
|
198
|
+
await driver.del(meta.sessionId);
|
|
199
|
+
}
|
|
200
|
+
return driver.write(nextSessionId, JSON.stringify(meta.data));
|
|
201
|
+
}
|
|
202
|
+
const session = { get, has, all, getCsrfToken, set, flash, reflash, del, flush, regenerate, invalidate, commit };
|
|
203
|
+
return session;
|
|
204
|
+
}
|
|
205
|
+
return { getSession };
|
|
206
|
+
}
|
|
207
|
+
function createScope(scopeKey) {
|
|
208
|
+
return (session) => {
|
|
209
|
+
function all() {
|
|
210
|
+
const rawData = session.get(scopeKey);
|
|
211
|
+
const data = rawData && typeof rawData === "object" ? rawData : {};
|
|
212
|
+
return { ...data };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
has(key) {
|
|
216
|
+
const rawData = session.get(scopeKey);
|
|
217
|
+
const data = rawData && typeof rawData === "object" ? rawData : {};
|
|
218
|
+
return key in data;
|
|
219
|
+
},
|
|
220
|
+
get(key) {
|
|
221
|
+
const rawData = session.get(scopeKey);
|
|
222
|
+
const data = rawData && typeof rawData === "object" ? rawData : {};
|
|
223
|
+
return data[key];
|
|
224
|
+
},
|
|
225
|
+
all,
|
|
226
|
+
set(key, value) {
|
|
227
|
+
const data = all();
|
|
228
|
+
data[key] = value;
|
|
229
|
+
session.set(scopeKey, data);
|
|
230
|
+
},
|
|
231
|
+
del(key) {
|
|
232
|
+
const data = all();
|
|
233
|
+
delete data[key];
|
|
234
|
+
session.set(scopeKey, data);
|
|
235
|
+
},
|
|
236
|
+
flush() {
|
|
237
|
+
session.del(scopeKey);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
var registry = ServiceRegistry.createRegistry(
|
|
243
|
+
ServiceRegistry.proxyFunctionsForObject
|
|
244
|
+
);
|
|
245
|
+
var Provider = {
|
|
246
|
+
create,
|
|
247
|
+
register: registry.register
|
|
248
|
+
};
|
|
249
|
+
function provider(key) {
|
|
250
|
+
const service = registry.resolve(key);
|
|
251
|
+
return {
|
|
252
|
+
getSession: service.getSession
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
var Session = {
|
|
256
|
+
Provider,
|
|
257
|
+
...provider("default"),
|
|
258
|
+
provider,
|
|
259
|
+
createScope,
|
|
260
|
+
drivers: {
|
|
261
|
+
cookie: cookieSessionDriver,
|
|
262
|
+
store: storeSessionDriver
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
export {
|
|
266
|
+
Session
|
|
267
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maestro-js/session",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"exports": {
|
|
5
|
+
".": {
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"default": "./dist/index.js"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"version": "1.0.0-alpha.0",
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "restricted"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@maestro-js/crypt": "1.0.0-alpha.0",
|
|
19
|
+
"@maestro-js/service-registry": "1.0.0-alpha.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.19.11"
|
|
23
|
+
},
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22.18.0"
|
|
27
|
+
},
|
|
28
|
+
"repository": "https://github.com/Marcato-Partners/maestro-js",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup --config ../../tsup.config.ts",
|
|
31
|
+
"typecheck": "tsc --noEmit",
|
|
32
|
+
"test": "beartest ./tests/**/*",
|
|
33
|
+
"format": "prettier --write src/",
|
|
34
|
+
"lint": "prettier --check src/"
|
|
35
|
+
}
|
|
36
|
+
}
|