@getstrata/bootstrap 0.2.49 → 0.2.51

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.
@@ -3,18 +3,71 @@ var __jsonParse = (a) => JSON.parse(a);
3
3
 
4
4
  // ../../src/bootstrap/web/session.ts
5
5
  import { createHash, randomBytes } from "crypto";
6
+ import { AuthManager } from "@getstrata/core/auth/guard";
7
+ import { getBoundDatabaseConnection } from "@getstrata/core/database/boundConnection";
8
+ import { getDefaultDatabasePool } from "@getstrata/core/database/defaultConnection";
6
9
  import { readRequestCookie } from "@getstrata/core/http/cookies";
10
+ function isSqlClient(value) {
11
+ return typeof value.unsafe === "function";
12
+ }
13
+ function resolveSql(source) {
14
+ if (isSqlClient(source)) {
15
+ return source;
16
+ }
17
+ return source();
18
+ }
19
+ function defaultSessionSql() {
20
+ const bound = getBoundDatabaseConnection();
21
+ if (bound) {
22
+ return bound;
23
+ }
24
+ return getDefaultDatabasePool();
25
+ }
26
+ function defaultMapSessionUser(user) {
27
+ return {
28
+ id: user.id,
29
+ role: user.is_admin ? "admin" : "member"
30
+ };
31
+ }
32
+ async function defaultLoadSessionUser(sql, sessionId) {
33
+ const rows = await sql.unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber,
34
+ COALESCE(u.is_admin, false) AS is_admin
35
+ FROM sessions s
36
+ INNER JOIN users u ON u.id = s.user_id
37
+ WHERE s.id = $1 AND s.expires_at > NOW()`, [sessionId]);
38
+ const row = rows[0];
39
+ if (!row)
40
+ return null;
41
+ return {
42
+ id: row.user_id,
43
+ name: row.name,
44
+ email: row.email,
45
+ learn_subscriber: row.learn_subscriber,
46
+ is_admin: row.is_admin
47
+ };
48
+ }
49
+ function redirectWithCookie(location, setCookie, status) {
50
+ return new Response(null, {
51
+ status,
52
+ headers: {
53
+ Location: location,
54
+ "Set-Cookie": setCookie
55
+ }
56
+ });
57
+ }
7
58
 
8
59
  class CookieSessionStore {
9
- sql;
60
+ sqlSource;
10
61
  secret;
11
62
  cookieName;
12
63
  maxAgeSeconds;
13
- constructor(sql, secret, cookieName = "strata_session", maxAgeSeconds = 60 * 60 * 24 * 14) {
14
- this.sql = sql;
64
+ loadSessionUser;
65
+ constructor(sqlSource, secret, cookieName = "strata_session", maxAgeSeconds = 60 * 60 * 24 * 14, loadSessionUser = defaultLoadSessionUser) {
66
+ this.sqlSource = sqlSource;
15
67
  this.secret = secret;
16
68
  this.cookieName = cookieName;
17
69
  this.maxAgeSeconds = maxAgeSeconds;
70
+ this.loadSessionUser = loadSessionUser;
18
71
  }
19
72
  cookieHeader(_user, sessionId) {
20
73
  const payload = `${sessionId}.${this.sign(sessionId)}`;
@@ -23,16 +76,30 @@ class CookieSessionStore {
23
76
  clearCookieHeader() {
24
77
  return this.withSecureFlag(`${this.cookieName}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`);
25
78
  }
79
+ sessionIdFromRequest(request) {
80
+ const cookie = readRequestCookie(request, this.cookieName);
81
+ const raw = cookie ?? null;
82
+ if (!raw)
83
+ return null;
84
+ const [sessionId, signature] = raw.split(".");
85
+ if (!sessionId || !signature || signature !== this.sign(sessionId)) {
86
+ return null;
87
+ }
88
+ return sessionId;
89
+ }
26
90
  withSecureFlag(header) {
27
91
  if (true) {
28
92
  return header;
29
93
  }
30
94
  return header.includes("Secure") ? header : `${header}; Secure`;
31
95
  }
96
+ sql() {
97
+ return resolveSql(this.sqlSource);
98
+ }
32
99
  async create(user) {
33
100
  const id = randomBytes(32).toString("hex");
34
101
  const expires = new Date(Date.now() + this.maxAgeSeconds * 1000);
35
- await this.sql.unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
102
+ await this.sql().unsafe(`INSERT INTO sessions (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
36
103
  id,
37
104
  user.id,
38
105
  expires
@@ -40,37 +107,70 @@ class CookieSessionStore {
40
107
  return id;
41
108
  }
42
109
  async destroy(sessionId) {
43
- await this.sql.unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
110
+ await this.sql().unsafe(`DELETE FROM sessions WHERE id = $1`, [sessionId]);
44
111
  }
45
112
  async read(request) {
46
- const cookie = readRequestCookie(request, this.cookieName);
47
- const raw = cookie ?? null;
48
- if (!raw)
49
- return null;
50
- const [sessionId, signature] = raw.split(".");
51
- if (!sessionId || !signature || signature !== this.sign(sessionId)) {
52
- return null;
53
- }
54
- const rows = await this.sql.unsafe(`SELECT s.id, s.user_id, s.expires_at, u.name, u.email, u.learn_subscriber,
55
- COALESCE(u.is_admin, false) AS is_admin
56
- FROM sessions s
57
- INNER JOIN users u ON u.id = s.user_id
58
- WHERE s.id = $1 AND s.expires_at > NOW()`, [sessionId]);
59
- const row = rows[0];
60
- if (!row)
113
+ const sessionId = this.sessionIdFromRequest(request);
114
+ if (!sessionId)
61
115
  return null;
62
- return {
63
- id: row.user_id,
64
- name: row.name,
65
- email: row.email,
66
- learn_subscriber: row.learn_subscriber,
67
- is_admin: row.is_admin
68
- };
116
+ return this.loadSessionUser(this.sql(), sessionId);
69
117
  }
70
118
  sign(value) {
71
119
  return createHash("sha256").update(`${value}.${this.secret}`).digest("hex").slice(0, 32);
72
120
  }
73
121
  }
122
+
123
+ class CookieSessionGuard {
124
+ store;
125
+ mapUser;
126
+ constructor(store, mapUser = defaultMapSessionUser) {
127
+ this.store = store;
128
+ this.mapUser = mapUser;
129
+ }
130
+ async resolve(request) {
131
+ const user = await this.store.read(request);
132
+ if (!user) {
133
+ return null;
134
+ }
135
+ return this.mapUser(user);
136
+ }
137
+ }
138
+
139
+ class CookieSessionAuthManager extends AuthManager {
140
+ store;
141
+ constructor(store, mapUser = defaultMapSessionUser) {
142
+ super(new CookieSessionGuard(store, mapUser));
143
+ this.store = store;
144
+ }
145
+ async signIn(user) {
146
+ const sessionId = await this.store.create(user);
147
+ return { sessionId, setCookie: this.store.cookieHeader(user, sessionId) };
148
+ }
149
+ async signOut(request) {
150
+ const sessionId = this.store.sessionIdFromRequest(request);
151
+ if (sessionId) {
152
+ await this.store.destroy(sessionId);
153
+ }
154
+ return { setCookie: this.store.clearCookieHeader() };
155
+ }
156
+ async signInRedirect(user, location, status = 302) {
157
+ const { setCookie } = await this.signIn(user);
158
+ return redirectWithCookie(location, setCookie, status);
159
+ }
160
+ async signOutRedirect(request, location, status = 302) {
161
+ const { setCookie } = await this.signOut(request);
162
+ return redirectWithCookie(location, setCookie, status);
163
+ }
164
+ }
165
+ function createCookieSessionAuthManager(options = {}) {
166
+ const store = options.store ?? new CookieSessionStore(options.sql ?? defaultSessionSql, options.secret ?? process.env.SESSION_SECRET?.trim() ?? "", options.cookieName, options.maxAgeSeconds, options.loadSessionUser);
167
+ return new CookieSessionAuthManager(store, options.mapUser ?? defaultMapSessionUser);
168
+ }
74
169
  export {
75
- CookieSessionStore
170
+ CookieSessionAuthManager,
171
+ CookieSessionGuard,
172
+ CookieSessionStore,
173
+ createCookieSessionAuthManager,
174
+ defaultMapSessionUser,
175
+ defaultSessionSql
76
176
  };