@byline/host-tanstack-start 2.3.1 → 2.3.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.
@@ -13,6 +13,11 @@ interface SignInPageProps {
13
13
  * layout (no app bar, breadcrumbs, or menu drawer). Wraps the
14
14
  * `SignInForm` from `@byline/ui` in the admin services provider so
15
15
  * the form can call `signIn` via the typed contract.
16
+ *
17
+ * Threads the configured `serverURL` into the `SignInForm` as `homeUrl` so
18
+ * the form's action row can render a plain "Home" link beside the submit
19
+ * button. After admin sign-out users land back here; the link is what lets
20
+ * them get back to the public site without typing the URL.
16
21
  */
17
22
  export declare function SignInPage({ callbackUrl }: SignInPageProps): import("react").JSX.Element;
18
23
  export {};
@@ -1,9 +1,11 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
+ import { getClientConfig } from "@byline/core";
2
3
  import { BylineAdminServicesProvider, SignInForm } from "@byline/ui/react";
3
4
  import classnames from "classnames";
4
5
  import { bylineAdminServices } from "../../integrations/byline-admin-services.js";
5
6
  import sign_in_page_module from "./sign-in-page.module.js";
6
7
  function SignInPage({ callbackUrl }) {
8
+ const { serverURL } = getClientConfig();
7
9
  return /*#__PURE__*/ jsx(BylineAdminServicesProvider, {
8
10
  services: bylineAdminServices,
9
11
  children: /*#__PURE__*/ jsx("main", {
@@ -11,7 +13,8 @@ function SignInPage({ callbackUrl }) {
11
13
  children: /*#__PURE__*/ jsx("div", {
12
14
  className: classnames('byline-sign-in-page-inner', sign_in_page_module.inner),
13
15
  children: /*#__PURE__*/ jsx(SignInForm, {
14
- callbackUrl: callbackUrl
16
+ callbackUrl: callbackUrl,
17
+ homeUrl: serverURL
15
18
  })
16
19
  })
17
20
  })
@@ -20,7 +20,7 @@ async function getAdminRequestContext() {
20
20
  } catch {}
21
21
  const refreshToken = readRefreshTokenCookie();
22
22
  if (!refreshToken) {
23
- clearSessionCookies();
23
+ if (accessToken) clearSessionCookies();
24
24
  throw ERR_UNAUTHENTICATED({
25
25
  message: 'no admin session'
26
26
  });
@@ -1,6 +1,7 @@
1
1
  import { createServerFn } from "@tanstack/react-start";
2
2
  import { getServerConfig } from "@byline/core";
3
3
  import { clearSessionCookies, readRefreshTokenCookie } from "../../auth/auth-cookies.js";
4
+ import { clearPreviewCookie } from "../../auth/preview-cookies.js";
4
5
  const adminSignOut = createServerFn({
5
6
  method: 'POST'
6
7
  }).handler(async ()=>{
@@ -10,6 +11,7 @@ const adminSignOut = createServerFn({
10
11
  await provider.revokeSession(refreshToken);
11
12
  } catch {}
12
13
  clearSessionCookies();
14
+ clearPreviewCookie();
13
15
  return {
14
16
  status: 'ok'
15
17
  };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "private": false,
4
4
  "type": "module",
5
5
  "license": "MPL-2.0",
6
- "version": "2.3.1",
6
+ "version": "2.3.3",
7
7
  "engines": {
8
8
  "node": ">=20.9.0"
9
9
  },
@@ -107,12 +107,12 @@
107
107
  "react-swipeable": "^7.0.2",
108
108
  "uuid": "^14.0.0",
109
109
  "zod": "^4.4.3",
110
- "@byline/ai": "2.3.1",
111
- "@byline/client": "2.3.1",
112
- "@byline/core": "2.3.1",
113
- "@byline/admin": "2.3.1",
114
- "@byline/auth": "2.3.1",
115
- "@byline/ui": "2.3.1"
110
+ "@byline/admin": "2.3.3",
111
+ "@byline/client": "2.3.3",
112
+ "@byline/auth": "2.3.3",
113
+ "@byline/core": "2.3.3",
114
+ "@byline/ai": "2.3.3",
115
+ "@byline/ui": "2.3.3"
116
116
  },
117
117
  "peerDependencies": {
118
118
  "@tanstack/react-router": "^1.167.0",
@@ -124,8 +124,8 @@
124
124
  "@biomejs/biome": "2.4.15",
125
125
  "@rsbuild/plugin-react": "^2.0.0",
126
126
  "@rslib/core": "^0.21.5",
127
- "@tanstack/react-router": "^1.170.6",
128
- "@tanstack/react-start": "^1.168.9",
127
+ "@tanstack/react-router": "^1.170.7",
128
+ "@tanstack/react-start": "^1.168.10",
129
129
  "@types/node": "^25.9.1",
130
130
  "@types/react": "19.2.15",
131
131
  "@types/react-dom": "19.2.3",
@@ -2,8 +2,11 @@
2
2
  * SignInPage — outer wrapper for the sign-in form.
3
3
  *
4
4
  * Override handles:
5
- * .byline-sign-in-page — outer <main>
5
+ * .byline-sign-in-page — outer <main>
6
6
  * .byline-sign-in-page-inner — vertically-centred card container
7
+ *
8
+ * The Home link itself lives inside `SignInForm`'s action row — see
9
+ * `@byline/ui` `sign-in-form.module.css` for `.byline-sign-in-home-link`.
7
10
  */
8
11
 
9
12
  .main,
@@ -6,6 +6,7 @@
6
6
  * Copyright (c) Infonomic Company Limited
7
7
  */
8
8
 
9
+ import { getClientConfig } from '@byline/core'
9
10
  import { BylineAdminServicesProvider, SignInForm } from '@byline/ui/react'
10
11
  import cx from 'classnames'
11
12
 
@@ -21,13 +22,19 @@ interface SignInPageProps {
21
22
  * layout (no app bar, breadcrumbs, or menu drawer). Wraps the
22
23
  * `SignInForm` from `@byline/ui` in the admin services provider so
23
24
  * the form can call `signIn` via the typed contract.
25
+ *
26
+ * Threads the configured `serverURL` into the `SignInForm` as `homeUrl` so
27
+ * the form's action row can render a plain "Home" link beside the submit
28
+ * button. After admin sign-out users land back here; the link is what lets
29
+ * them get back to the public site without typing the URL.
24
30
  */
25
31
  export function SignInPage({ callbackUrl }: SignInPageProps) {
32
+ const { serverURL } = getClientConfig()
26
33
  return (
27
34
  <BylineAdminServicesProvider services={bylineAdminServices}>
28
35
  <main className={cx('byline-sign-in-page', styles.main)}>
29
36
  <div className={cx('byline-sign-in-page-inner', styles.inner)}>
30
- <SignInForm callbackUrl={callbackUrl} />
37
+ <SignInForm callbackUrl={callbackUrl} homeUrl={serverURL} />
31
38
  </div>
32
39
  </main>
33
40
  </BylineAdminServicesProvider>
@@ -137,7 +137,7 @@ describe('getAdminRequestContext', () => {
137
137
  expect(verifyAccessToken).toHaveBeenCalledWith('fresh-access')
138
138
  })
139
139
 
140
- it('throws ERR_UNAUTHENTICATED and clears cookies when no session exists', async () => {
140
+ it('throws ERR_UNAUTHENTICATED without emitting Set-Cookie when no cookies are sent', async () => {
141
141
  cookiesReturn({})
142
142
 
143
143
  try {
@@ -147,7 +147,22 @@ describe('getAdminRequestContext', () => {
147
147
  expect(err).toBeInstanceOf(AuthError)
148
148
  expect((err as AuthError).code).toBe(AuthErrorCodes.UNAUTHENTICATED)
149
149
  }
150
- // Cookie clear = setCookie with empty value and maxAge 0 for both names.
150
+ // Anonymous visitors must produce zero Set-Cookie headers so shared
151
+ // caches (Cloudflare) can cache public pages — a Set-Cookie on the
152
+ // response is a hard bypass signal for CDNs.
153
+ expect(setCookie).not.toHaveBeenCalled()
154
+ })
155
+
156
+ it('clears the stale access cookie when only an access token was sent', async () => {
157
+ cookiesReturn({ byline_access_token: 'stale' })
158
+ verifyAccessToken.mockRejectedValueOnce(new Error('expired'))
159
+
160
+ try {
161
+ await getAdminRequestContext()
162
+ expect.fail('expected ERR_UNAUTHENTICATED')
163
+ } catch (err) {
164
+ expect((err as AuthError).code).toBe(AuthErrorCodes.UNAUTHENTICATED)
165
+ }
151
166
  const clears = setCookie.mock.calls.filter((c) => c[2]?.maxAge === 0)
152
167
  const clearedNames = new Set(clears.map((c) => c[0]))
153
168
  expect(clearedNames.has('byline_access_token')).toBe(true)
@@ -80,8 +80,13 @@ export async function getAdminRequestContext(): Promise<RequestContext> {
80
80
  // Refresh path: swap the refresh cookie for a fresh token pair.
81
81
  const refreshToken = readRefreshTokenCookie()
82
82
  if (!refreshToken) {
83
- // No session at all clear anything stale and reject.
84
- clearSessionCookies()
83
+ // Only emit cookie clears when the browser actually sent a stale access
84
+ // cookie — otherwise the response carries no Set-Cookie at all, which
85
+ // lets shared caches (Cloudflare) cache public pages for anonymous
86
+ // visitors. Set-Cookie on the response is a hard bypass signal for CDNs.
87
+ if (accessToken) {
88
+ clearSessionCookies()
89
+ }
85
90
  throw ERR_UNAUTHENTICATED({ message: 'no admin session' })
86
91
  }
87
92
 
@@ -9,10 +9,16 @@
9
9
  /**
10
10
  * Admin sign-out server function.
11
11
  *
12
- * Revokes the current refresh token (so a stolen copy cannot be reused)
13
- * and clears both session cookies. Idempotent if the caller already
14
- * lacks a refresh cookie, we still clear whatever's there and return
15
- * successfully.
12
+ * Revokes the current refresh token (so a stolen copy cannot be reused),
13
+ * clears both session cookies, and clears the preview-mode cookie.
14
+ * Idempotent — if the caller already lacks a refresh cookie, we still
15
+ * clear whatever's there and return successfully.
16
+ *
17
+ * Note: clearing `byline_preview` here is hygiene, not a security
18
+ * requirement. The CDN cache-bypass middleware keys off the session
19
+ * cookies, not the preview cookie, so a stale preview cookie left in the
20
+ * browser does not affect cacheability of subsequent anonymous responses.
21
+ * Clearing it simply means the next sign-in starts in non-preview mode.
16
22
  */
17
23
 
18
24
  import { createServerFn } from '@tanstack/react-start'
@@ -20,6 +26,7 @@ import { createServerFn } from '@tanstack/react-start'
20
26
  import { getServerConfig } from '@byline/core'
21
27
 
22
28
  import { clearSessionCookies, readRefreshTokenCookie } from '../../auth/auth-cookies.js'
29
+ import { clearPreviewCookie } from '../../auth/preview-cookies.js'
23
30
 
24
31
  export const adminSignOut = createServerFn({ method: 'POST' }).handler(async () => {
25
32
  const provider = getServerConfig().sessionProvider
@@ -38,5 +45,6 @@ export const adminSignOut = createServerFn({ method: 'POST' }).handler(async ()
38
45
  }
39
46
 
40
47
  clearSessionCookies()
48
+ clearPreviewCookie()
41
49
  return { status: 'ok' as const }
42
50
  })