@antelopejs/dms-frontend 0.5.1 → 0.5.3

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.5.1",
3
+ "version": "0.5.3",
4
4
  "description": "Frontend-agnostic loader for AntelopeJS DMS, shipping the Vue 3 renderer (Vite, Inertia, SSR)",
5
5
  "keywords": [
6
6
  "antelope",
@@ -537,11 +537,54 @@ function parseRoute(runtime: DmsFrontendRuntime, url: string): DmsRoute {
537
537
  };
538
538
  }
539
539
 
540
+ function hasSameEntries(
541
+ left: Record<string, string>,
542
+ right: Record<string, string>,
543
+ ): boolean {
544
+ const keys = Object.keys(left);
545
+ return (
546
+ keys.length === Object.keys(right).length &&
547
+ keys.every((key) => right[key] === left[key])
548
+ );
549
+ }
550
+
551
+ // Every parse builds a new query, params and matched: assigning them as they
552
+ // come would re-run each watcher of the route on every call of useDmsRoute,
553
+ // though the URL did not change. They are only replaced when their content did.
540
554
  function updateRoute(runtime: DmsFrontendRuntime, url: string): void {
541
555
  // The meta is the page's, not the URL's: a page declares it, then reads its
542
556
  // route, and the middleware the meta selected must still find it.
543
- const { meta: _pageMeta, ...location } = parseRoute(runtime, url);
544
- Object.assign(runtime.route, location);
557
+ const {
558
+ meta: _pageMeta,
559
+ query,
560
+ params,
561
+ matched,
562
+ ...location
563
+ } = parseRoute(runtime, url);
564
+ const route = runtime.route;
565
+ if (!hasSameEntries(query, route.query)) route.query = query;
566
+ if (!hasSameEntries(params, route.params)) route.params = params;
567
+ if (matched.length !== route.matched.length) route.matched = matched;
568
+ Object.assign(route, location);
569
+ }
570
+
571
+ const URL_BASE = "http://frontend.local";
572
+
573
+ // The URL the browser shows: it may be ahead of the route, which follows
574
+ // Inertia's page, when a component wrote it in place (history.replaceState).
575
+ function currentUrl(runtime: DmsFrontendRuntime): string {
576
+ if (runtime.isServer || typeof window === "undefined")
577
+ return new URL(runtime.route.fullPath, URL_BASE).href;
578
+ return window.location.href;
579
+ }
580
+
581
+ // One spelling per URL: `a b`, `a+b` and `a%20b` are the same query, as are
582
+ // `:` and `%3A`. The origin is kept, so a URL of another site never matches
583
+ // the current one, whatever its path.
584
+ function normalizeUrl(url: string, base: string): string {
585
+ const parsed = new URL(url, base);
586
+ const search = parsed.searchParams.toString();
587
+ return `${parsed.origin}${parsed.pathname}${search ? `?${search}` : ""}${parsed.hash}`;
545
588
  }
546
589
 
547
590
  function locationToUrl(
@@ -645,10 +688,13 @@ export function useDmsRoute(pattern?: string): DmsRoute {
645
688
 
646
689
  export function useDmsRouter(): DmsRouter {
647
690
  const runtime = useDmsRuntime();
648
- const navigate = (to: RouteLocationRaw, options?: DmsNavigationOptions) =>
649
- locationToUrl(runtime, to) === runtime.route.fullPath
691
+ const navigate = (to: RouteLocationRaw, options?: DmsNavigationOptions) => {
692
+ const current = currentUrl(runtime);
693
+ return normalizeUrl(locationToUrl(runtime, to), current) ===
694
+ normalizeUrl(current, current)
650
695
  ? Promise.resolve()
651
696
  : visit(runtime, to, options);
697
+ };
652
698
  return {
653
699
  currentRoute: computed(() => runtime.route),
654
700
  push: navigate,
@@ -14,6 +14,32 @@ const MAX_AGE = 60 * 60 * 24 * 30;
14
14
  const UUID =
15
15
  /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
16
16
 
17
+ /**
18
+ * A suffix that keeps this instance's cookies apart from another's on the same
19
+ * host. Browsers scope cookies by hostname, not port: without it, two DMS
20
+ * frontends on localhost:3000 and localhost:3001 would overwrite each other's
21
+ * session, each failing to decrypt the other's and signing the user out. The
22
+ * suffix follows the backend, so a restart keeps the cookies it wrote.
23
+ */
24
+ function cookieScope() {
25
+ const backendUrl = (process.env.DMS_API_BASE_URL ?? "").trim();
26
+ let canonical = backendUrl;
27
+ try {
28
+ const url = new URL(backendUrl);
29
+ if (url.hostname === "localhost" || url.hostname === "[::1]")
30
+ url.hostname = "127.0.0.1";
31
+ canonical = url.toString().replace(/\/$/, "");
32
+ } catch {
33
+ // Not a URL: the raw string still tells two backends apart.
34
+ }
35
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 12);
36
+ }
37
+
38
+ /** The name `name` takes in this instance's cookie jar. */
39
+ export function scopedCookieName(name) {
40
+ return `${name}_${cookieScope()}`;
41
+ }
42
+
17
43
  function key() {
18
44
  const secret = process.env.DMS_SESSION_SECRET;
19
45
  if (!secret || secret.length < 32)
@@ -54,10 +80,11 @@ function decode(value) {
54
80
  }
55
81
 
56
82
  function cookieValue(request, name) {
83
+ const scoped = scopedCookieName(name);
57
84
  const cookies = request.headers.cookie?.split(";") ?? [];
58
85
  return cookies
59
86
  .map((part) => part.trim().split("="))
60
- .find(([key]) => key === name)?.[1];
87
+ .find(([key]) => key === scoped)?.[1];
61
88
  }
62
89
 
63
90
  /**
@@ -83,7 +110,8 @@ function lifetime(persistent) {
83
110
  return persistent ? MAX_AGE : undefined;
84
111
  }
85
112
 
86
- function appendCookie(response, cookie) {
113
+ function appendCookie(response, name, value, cookieAttributes) {
114
+ const cookie = `${scopedCookieName(name)}=${value}; ${cookieAttributes}`;
87
115
  const current = response.getHeader("set-cookie");
88
116
  response.setHeader(
89
117
  "set-cookie",
@@ -122,12 +150,14 @@ export function setRequestSession(request, session) {
122
150
  export function writeSession(response, session) {
123
151
  appendCookie(
124
152
  response,
125
- `${SESSION_COOKIE}=${encode(session)}; ${attributes(lifetime(isPersistent(session)))}`,
153
+ SESSION_COOKIE,
154
+ encode(session),
155
+ attributes(lifetime(isPersistent(session))),
126
156
  );
127
157
  }
128
158
 
129
159
  export function clearSession(response) {
130
- appendCookie(response, `${SESSION_COOKIE}=; ${attributes(0)}`);
160
+ appendCookie(response, SESSION_COOKIE, "", attributes(0));
131
161
  }
132
162
 
133
163
  export function readAccounts(request) {
@@ -139,7 +169,9 @@ function writeAccountIndex(response, accounts) {
139
169
  const persistent = Object.values(accounts).some(isPersistent);
140
170
  appendCookie(
141
171
  response,
142
- `${ACCOUNT_INDEX_COOKIE}=${encode(accounts)}; ${attributes(lifetime(persistent))}`,
172
+ ACCOUNT_INDEX_COOKIE,
173
+ encode(accounts),
174
+ attributes(lifetime(persistent)),
143
175
  );
144
176
  }
145
177
 
@@ -156,7 +188,9 @@ export function storeAccount(request, response, session, accountId) {
156
188
  writeAccountIndex(response, accounts);
157
189
  appendCookie(
158
190
  response,
159
- `${accountCookieName(id)}=${encode({ refreshToken: session.refreshToken, ...accountDescriptor })}; ${attributes(lifetime(accountDescriptor.persistent))}`,
191
+ accountCookieName(id),
192
+ encode({ refreshToken: session.refreshToken, ...accountDescriptor }),
193
+ attributes(lifetime(accountDescriptor.persistent)),
160
194
  );
161
195
  return accountDescriptor;
162
196
  }
@@ -186,10 +220,7 @@ export function removeAccount(request, response, accountId) {
186
220
  delete accounts[accountId];
187
221
  writeAccountIndex(response, accounts);
188
222
  if (UUID.test(accountId ?? ""))
189
- appendCookie(
190
- response,
191
- `${accountCookieName(accountId)}=; ${attributes(0)}`,
192
- );
223
+ appendCookie(response, accountCookieName(accountId), "", attributes(0));
193
224
  }
194
225
 
195
226
  export function readSealedCookie(request, name) {
@@ -204,12 +235,9 @@ export function writeSealedCookie(
204
235
  maxAge,
205
236
  path = "/auth/oauth",
206
237
  ) {
207
- appendCookie(
208
- response,
209
- `${name}=${encode(value)}; ${attributes(maxAge, path)}`,
210
- );
238
+ appendCookie(response, name, encode(value), attributes(maxAge, path));
211
239
  }
212
240
 
213
241
  export function clearSealedCookie(response, name, path = "/auth/oauth") {
214
- appendCookie(response, `${name}=; ${attributes(0, path)}`);
242
+ appendCookie(response, name, "", attributes(0, path));
215
243
  }