@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.
- package/dist/admin-shell/chrome/sign-in-page.d.ts +5 -0
- package/dist/admin-shell/chrome/sign-in-page.js +4 -1
- package/dist/auth/auth-context.js +1 -1
- package/dist/server-fns/auth/sign-out.js +2 -0
- package/package.json +9 -9
- package/src/admin-shell/chrome/sign-in-page.module.css +4 -1
- package/src/admin-shell/chrome/sign-in-page.tsx +8 -1
- package/src/auth/auth-context.test.node.ts +17 -2
- package/src/auth/auth-context.ts +7 -2
- package/src/server-fns/auth/sign-out.ts +12 -4
|
@@ -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.
|
|
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/
|
|
111
|
-
"@byline/client": "2.3.
|
|
112
|
-
"@byline/
|
|
113
|
-
"@byline/
|
|
114
|
-
"@byline/
|
|
115
|
-
"@byline/ui": "2.3.
|
|
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.
|
|
128
|
-
"@tanstack/react-start": "^1.168.
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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)
|
package/src/auth/auth-context.ts
CHANGED
|
@@ -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
|
-
//
|
|
84
|
-
|
|
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
|
-
*
|
|
14
|
-
* lacks a refresh cookie, we still
|
|
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
|
})
|