@antelopejs/dms-frontend 0.10.4 → 0.10.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antelopejs/dms-frontend",
3
- "version": "0.10.4",
3
+ "version": "0.10.5",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -4,6 +4,7 @@ import {
4
4
  createHash,
5
5
  randomBytes,
6
6
  } from "node:crypto";
7
+ import { isBoolean, isNumber, isRecord, isString } from "../type-guards.mjs";
7
8
 
8
9
  export const SESSION_COOKIE = "dms_session";
9
10
  const REQUEST_SESSION = Symbol("dms-request-session");
@@ -66,12 +67,17 @@ function key() {
66
67
  }
67
68
 
68
69
  /**
70
+ * Seals `value` for the cookie `name` alone: the name is the AES-GCM
71
+ * additional authenticated data, so the value opens under no other cookie.
72
+ *
73
+ * @param {string} name The cookie's name in the jar
69
74
  * @param {unknown} value A session, or the value of a module's sealed cookie
70
75
  * @returns {string}
71
76
  */
72
- function encode(value) {
77
+ function encode(name, value) {
73
78
  const iv = randomBytes(12);
74
79
  const cipher = createCipheriv("aes-256-gcm", key(), iv);
80
+ cipher.setAAD(Buffer.from(name));
75
81
  const encrypted = Buffer.concat([
76
82
  cipher.update(JSON.stringify(value)),
77
83
  cipher.final(),
@@ -82,11 +88,12 @@ function encode(value) {
82
88
  }
83
89
 
84
90
  /**
91
+ * @param {string} name The cookie's name in the jar
85
92
  * @param {string} value The cookie's value
86
- * @returns {any} What was sealed, as the JSON it was written from, `undefined`
87
- * when it does not open
93
+ * @returns {unknown} What was sealed for `name`, as the JSON it was written
94
+ * from, `undefined` when it does not open
88
95
  */
89
- function decode(value) {
96
+ function decode(name, value) {
90
97
  try {
91
98
  const payload = Buffer.from(value, "base64url");
92
99
  const decipher = createDecipheriv(
@@ -95,6 +102,7 @@ function decode(value) {
95
102
  payload.subarray(0, 12),
96
103
  );
97
104
  decipher.setAuthTag(payload.subarray(12, 28));
105
+ decipher.setAAD(Buffer.from(name));
98
106
  return JSON.parse(
99
107
  Buffer.concat([
100
108
  decipher.update(payload.subarray(28)),
@@ -107,16 +115,21 @@ function decode(value) {
107
115
  }
108
116
 
109
117
  /**
110
- * @param {import("node:http").IncomingMessage} request
111
- * @param {string} name
112
- * @returns {string | undefined}
118
+ * @param {unknown} value
119
+ * @returns {value is Session} Whether `value` is what `writeSession` seals
113
120
  */
114
- function cookieValue(request, name) {
115
- const scoped = scopedCookieName(name);
116
- const cookies = request.headers.cookie?.split(";") ?? [];
117
- return cookies
118
- .map((part) => part.trim().split("="))
119
- .find(([key]) => key === scoped)?.[1];
121
+ function isSession(value) {
122
+ if (!isRecord(value) || !isBoolean(value.persistent)) return false;
123
+ const { user } = value;
124
+ return (
125
+ ["accessToken", "refreshToken", "accountId", "activeTenantId"].every(
126
+ (field) => !Object.hasOwn(value, field) || isString(value[field]),
127
+ ) &&
128
+ (!Object.hasOwn(value, "updatedAt") || isNumber(value.updatedAt)) &&
129
+ (!Object.hasOwn(value, "user") ||
130
+ (isRecord(user) &&
131
+ (!Object.hasOwn(user, "language") || isString(user.language))))
132
+ );
120
133
  }
121
134
 
122
135
  /**
@@ -158,6 +171,21 @@ function appendCookie(response, name, value, cookieAttributes) {
158
171
  );
159
172
  }
160
173
 
174
+ /**
175
+ * @param {import("node:http").ServerResponse} response
176
+ * @param {string} name
177
+ * @param {unknown} value
178
+ * @param {string} cookieAttributes
179
+ */
180
+ function appendSealedCookie(response, name, value, cookieAttributes) {
181
+ appendCookie(
182
+ response,
183
+ name,
184
+ encode(scopedCookieName(name), value),
185
+ cookieAttributes,
186
+ );
187
+ }
188
+
161
189
  /**
162
190
  * @param {SessionRequest} request
163
191
  * @returns {Session | undefined} The session set for the request, else the one
@@ -165,9 +193,8 @@ function appendCookie(response, name, value, cookieAttributes) {
165
193
  */
166
194
  export function readSession(request) {
167
195
  if (REQUEST_SESSION in request) return request[REQUEST_SESSION];
168
- const value = cookieValue(request, SESSION_COOKIE);
169
- // oxlint-disable-next-line typescript/no-unsafe-return -- `decode` returns the JSON this server sealed under `SESSION_COOKIE`, untyped because `JSON.parse` is; the AES-GCM tag rejects any cookie it did not write.
170
- return value ? decode(value) : undefined;
196
+ const sealed = readSealedCookie(request, SESSION_COOKIE);
197
+ return isSession(sealed) ? sealed : undefined;
171
198
  }
172
199
 
173
200
  /**
@@ -183,10 +210,10 @@ export function setRequestSession(request, session) {
183
210
  * @param {Session} session
184
211
  */
185
212
  export function writeSession(response, session) {
186
- appendCookie(
213
+ appendSealedCookie(
187
214
  response,
188
215
  SESSION_COOKIE,
189
- encode(session),
216
+ session,
190
217
  attributes(lifetime(session)),
191
218
  );
192
219
  }
@@ -203,8 +230,12 @@ export function clearSession(response) {
203
230
  * `undefined` when there is none or it does not open
204
231
  */
205
232
  export function readSealedCookie(request, name) {
206
- const value = cookieValue(request, name);
207
- return value ? decode(value) : undefined;
233
+ const scoped = scopedCookieName(name);
234
+ const cookies = request.headers.cookie?.split(";") ?? [];
235
+ const value = cookies
236
+ .map((part) => part.trim().split("="))
237
+ .find(([key]) => key === scoped)?.[1];
238
+ return value ? decode(scoped, value) : undefined;
208
239
  }
209
240
 
210
241
  /**
@@ -215,7 +246,7 @@ export function readSealedCookie(request, name) {
215
246
  * @param {string} [path]
216
247
  */
217
248
  export function writeSealedCookie(response, name, value, maxAge, path = "/") {
218
- appendCookie(response, name, encode(value), attributes(maxAge, path));
249
+ appendSealedCookie(response, name, value, attributes(maxAge, path));
219
250
  }
220
251
 
221
252
  /**