@carvajalconsultants/headstart 1.0.4 → 1.0.6
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/README.md +115 -28
- package/cookies.ts +86 -0
- package/grafastExchange.ts +61 -7
- package/package.json +4 -2
- package/serverCookies.ts +160 -0
package/README.md
CHANGED
|
@@ -16,15 +16,15 @@ This library is made up of:
|
|
|
16
16
|
Specifically, the `graphile.config.ts` and `pgl.ts`. If you need WebSocket support, make sure to add a plugin in `graphile.config.ts` as per: https://postgraphile.org/postgraphile/next/subscriptions
|
|
17
17
|
|
|
18
18
|
3. Install Headtart & URQL:
|
|
19
|
-
```
|
|
20
|
-
yarn add @carvajalconsultants/headstart urql @urql/exchange-graphcache
|
|
19
|
+
```bash
|
|
20
|
+
yarn add @carvajalconsultants/headstart urql @urql/exchange-graphcache @urql/exchange-auth
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
4. Enable WebSockets in `app.config.ts` if you need it:
|
|
24
24
|
|
|
25
|
-
```
|
|
25
|
+
```typescript
|
|
26
26
|
// app.config.ts
|
|
27
|
-
import { defineConfig } from "@tanstack/start/config";
|
|
27
|
+
import { defineConfig } from "@tanstack/react-start/config";
|
|
28
28
|
|
|
29
29
|
export default defineConfig({
|
|
30
30
|
server: {
|
|
@@ -37,7 +37,7 @@ export default defineConfig({
|
|
|
37
37
|
|
|
38
38
|
5. Make sure to add the `/api` endpoint to the grafserv configuration so that Ruru GraphQL client works correctly:
|
|
39
39
|
|
|
40
|
-
```
|
|
40
|
+
```typescript
|
|
41
41
|
// graphile.config.ts
|
|
42
42
|
const preset: GraphileConfig.Preset = {
|
|
43
43
|
...
|
|
@@ -52,9 +52,9 @@ const preset: GraphileConfig.Preset = {
|
|
|
52
52
|
|
|
53
53
|
6. Add the api.ts file so that it calls our GraphQL handler. This will receive all GraphQL requests at the /api endpoint.
|
|
54
54
|
|
|
55
|
-
```
|
|
55
|
+
```typescript
|
|
56
56
|
// app/api.ts
|
|
57
|
-
import { defaultAPIFileRouteHandler } from "@tanstack/start/api";
|
|
57
|
+
import { defaultAPIFileRouteHandler } from "@tanstack/react-start/api";
|
|
58
58
|
import { createStartAPIHandler } from "@carvajalconsultants/headstart/server";
|
|
59
59
|
import { pgl } from "../pgl";
|
|
60
60
|
|
|
@@ -63,7 +63,7 @@ export default createStartAPIHandler(pgl, defaultAPIFileRouteHandler);
|
|
|
63
63
|
|
|
64
64
|
7. Now we need to configure URQL for client and server rendering, first we start with the server. Create this provider:
|
|
65
65
|
|
|
66
|
-
```
|
|
66
|
+
```typescript
|
|
67
67
|
// app/graphql/serverProvider.tsx
|
|
68
68
|
import { Client } from "urql";
|
|
69
69
|
import { grafastExchange } from "@carvajalconsultants/headstart/server";
|
|
@@ -81,9 +81,90 @@ export const client = new Client({
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
8.
|
|
84
|
+
8. Now the client side for URQL:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// app/graphql/clientProvider.tsx
|
|
88
|
+
import { ssr } from "@carvajalconsultants/headstart/client";
|
|
89
|
+
import { authExchange } from "@urql/exchange-auth";
|
|
90
|
+
import { cacheExchange } from "@urql/exchange-graphcache";
|
|
91
|
+
//import { relayPagination } from "@urql/exchange-graphcache/extras";
|
|
92
|
+
import { Client, fetchExchange } from "urql";
|
|
85
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Creates an authentication exchange for handling secure GraphQL operations.
|
|
96
|
+
* This exchange ensures that all GraphQL requests are properly authenticated
|
|
97
|
+
* and handles authentication failures gracefully.
|
|
98
|
+
*
|
|
99
|
+
* @returns {Object} Authentication configuration object
|
|
100
|
+
* @returns {Function} .addAuthToOperation - Prepares operations with auth context
|
|
101
|
+
* @returns {Function} .didAuthError - Detects authentication failures after server request
|
|
102
|
+
* @returns {Function} .refreshAuth - Handles auth token refresh
|
|
103
|
+
*/
|
|
104
|
+
const auth = authExchange(async () => {
|
|
105
|
+
//TODO Implement authentication checking for the client s ide
|
|
106
|
+
await new Promise((resolve) => {
|
|
107
|
+
setTimeout(() => {
|
|
108
|
+
console.log("IMPLEMENT CLIENT AUTH CHECK");
|
|
109
|
+
resolve();
|
|
110
|
+
}, 2000);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
/**
|
|
115
|
+
* Processes each GraphQL operation to include authentication context.
|
|
116
|
+
* Currently configured as a pass-through as tokens are in cookies.
|
|
117
|
+
*
|
|
118
|
+
* @param {Operation} operation - The GraphQL operation to authenticate
|
|
119
|
+
* @returns {Operation} The operation with authentication context
|
|
120
|
+
*/
|
|
121
|
+
addAuthToOperation: (operation) => operation,
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Identifies when an operation has failed due to authentication issues.
|
|
125
|
+
* Used to trigger authentication refresh flows when needed.
|
|
126
|
+
*
|
|
127
|
+
* @param {Error} error - The GraphQL error response
|
|
128
|
+
* @returns {boolean} True if the error was caused by authentication failure
|
|
129
|
+
*/
|
|
130
|
+
didAuthError: (error) => error.graphQLErrors.some((e) => e.extensions?.code === "FORBIDDEN"),
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Handles refreshing authentication when it becomes invalid.
|
|
134
|
+
* Currently implemented as a no-op as token refresh is handled by getSession().
|
|
135
|
+
*/
|
|
136
|
+
refreshAuth: async () => {
|
|
137
|
+
/* No-op, this is done in getSession() */
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Configured GraphQL client for the application.
|
|
144
|
+
* Provides a centralized way to make authenticated GraphQL requests with
|
|
145
|
+
* proper caching and server-side rendering support.
|
|
146
|
+
*/
|
|
147
|
+
export const client = new Client({
|
|
148
|
+
url: "http://localhost:3000/api/graphql",
|
|
149
|
+
exchanges: [
|
|
150
|
+
cacheExchange({
|
|
151
|
+
resolvers: {
|
|
152
|
+
Query: {
|
|
153
|
+
// Implements relay-style pagination for fills pending match
|
|
154
|
+
//queryName: relayPagination(),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
auth,
|
|
159
|
+
ssr,
|
|
160
|
+
fetchExchange,
|
|
161
|
+
],
|
|
162
|
+
});
|
|
86
163
|
```
|
|
164
|
+
|
|
165
|
+
9. Create the server side router which uses Grafast to execute queries:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
87
168
|
// app/serverRouter.tsx
|
|
88
169
|
import { ssr } from "@carvajalconsultants/headstart/client";
|
|
89
170
|
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
|
@@ -91,6 +172,8 @@ import { Provider } from "urql";
|
|
|
91
172
|
import { client } from "./graphql/serverProvider";
|
|
92
173
|
import { routeTree } from "./routeTree.gen";
|
|
93
174
|
|
|
175
|
+
import type { ReactNode } from "react";
|
|
176
|
+
|
|
94
177
|
export function createRouter() {
|
|
95
178
|
const router = createTanStackRouter({
|
|
96
179
|
routeTree,
|
|
@@ -101,20 +184,21 @@ export function createRouter() {
|
|
|
101
184
|
// Send data to client so URQL can be hydrated.
|
|
102
185
|
dehydrate: () => ({ initialData: ssr.extractData() }),
|
|
103
186
|
|
|
104
|
-
|
|
105
|
-
Wrap: ({ children }) => <Provider value={client}>{children}</Provider>,
|
|
187
|
+
// Wrap our entire route with the URQL provider so we can execute queries and mutations.
|
|
188
|
+
Wrap: ({ children }: { children: ReactNode }) => <Provider value={client}>{children}</Provider>,
|
|
106
189
|
});
|
|
107
190
|
|
|
108
191
|
return router;
|
|
109
192
|
}
|
|
110
193
|
```
|
|
111
194
|
|
|
112
|
-
|
|
195
|
+
10. Modify the TSR server-side rendering function to use this new router:
|
|
113
196
|
|
|
114
|
-
```
|
|
197
|
+
```typescript
|
|
198
|
+
/* eslint-disable */
|
|
115
199
|
// app/ssr.tsx
|
|
116
200
|
/// <reference types="vinxi/types/server" />
|
|
117
|
-
import { getRouterManifest } from "@tanstack/start/router-manifest";
|
|
201
|
+
import { getRouterManifest } from "@tanstack/react-start/router-manifest";
|
|
118
202
|
import {
|
|
119
203
|
createStartHandler,
|
|
120
204
|
defaultStreamHandler,
|
|
@@ -128,9 +212,9 @@ export default createStartHandler({
|
|
|
128
212
|
})(defaultStreamHandler);
|
|
129
213
|
```
|
|
130
214
|
|
|
131
|
-
|
|
215
|
+
11. Add the client side router which uses the fetch exchange to execute queries, mutations, etc.:
|
|
132
216
|
|
|
133
|
-
```
|
|
217
|
+
```typescript
|
|
134
218
|
// app/clientRouter.tsx
|
|
135
219
|
import { ssr } from "@carvajalconsultants/headstart/client";
|
|
136
220
|
import { createRouter as createTanStackRouter } from "@tanstack/react-router";
|
|
@@ -138,6 +222,9 @@ import { Provider } from "urql";
|
|
|
138
222
|
import { client } from "./graphql/clientProvider";
|
|
139
223
|
import { routeTree } from "./routeTree.gen";
|
|
140
224
|
|
|
225
|
+
import type { ReactNode } from "react";
|
|
226
|
+
import type { SSRData } from "urql";
|
|
227
|
+
|
|
141
228
|
export function createRouter() {
|
|
142
229
|
const router = createTanStackRouter({
|
|
143
230
|
routeTree,
|
|
@@ -146,20 +233,20 @@ export function createRouter() {
|
|
|
146
233
|
},
|
|
147
234
|
hydrate: (dehydrated) => {
|
|
148
235
|
// Hydrate URQL with data passed by TSR, this is generated by dehydrate function in server router.
|
|
149
|
-
ssr.restoreData(dehydrated.initialData);
|
|
236
|
+
ssr.restoreData(dehydrated.initialData as SSRData);
|
|
150
237
|
},
|
|
151
238
|
|
|
152
|
-
|
|
153
|
-
Wrap: ({ children }) => <Provider value={client}>{children}</Provider>,
|
|
239
|
+
// Wrap our entire route with the URQL provider so we can execute queries and mutations.
|
|
240
|
+
Wrap: ({ children }: { children: ReactNode }) => <Provider value={client}>{children}</Provider>,
|
|
154
241
|
});
|
|
155
242
|
|
|
156
243
|
return router;
|
|
157
244
|
}
|
|
158
245
|
```
|
|
159
246
|
|
|
160
|
-
|
|
247
|
+
12. Tell TSR to use our client side router:
|
|
161
248
|
|
|
162
|
-
```
|
|
249
|
+
```typescript
|
|
163
250
|
// app/client.tsx
|
|
164
251
|
/// <reference types="vinxi/types/client" />
|
|
165
252
|
import { StartClient } from "@tanstack/start";
|
|
@@ -171,9 +258,9 @@ const router = createRouter();
|
|
|
171
258
|
hydrateRoot(document.getElementById("root")!, <StartClient router={router} />);
|
|
172
259
|
```
|
|
173
260
|
|
|
174
|
-
|
|
261
|
+
13. Last but not least, you're ready to start using URQL on your components and pages. First we create the route using the loader option so we can pre-load data:
|
|
175
262
|
|
|
176
|
-
```
|
|
263
|
+
```typescript
|
|
177
264
|
export const Route = createFileRoute("/")({
|
|
178
265
|
...
|
|
179
266
|
validateSearch: zodSearchValidator(paramSchema),
|
|
@@ -188,9 +275,9 @@ export const Route = createFileRoute("/")({
|
|
|
188
275
|
});
|
|
189
276
|
```
|
|
190
277
|
|
|
191
|
-
|
|
278
|
+
14. Now in your component, you can query with URQL as you normally would:
|
|
192
279
|
|
|
193
|
-
```
|
|
280
|
+
```typescript
|
|
194
281
|
const Home = () => {
|
|
195
282
|
const { page } = Route.useSearch();
|
|
196
283
|
|
|
@@ -209,8 +296,8 @@ const Home = () => {
|
|
|
209
296
|
|
|
210
297
|
## Deployment
|
|
211
298
|
|
|
212
|
-
1. Run `
|
|
299
|
+
1. Run `yarn run build`
|
|
213
300
|
2. `cd .output/server`
|
|
214
301
|
3. `rm -rf node_modules`
|
|
215
|
-
4. `
|
|
216
|
-
5. `
|
|
302
|
+
4. `yarn install`
|
|
303
|
+
5. `yarn run index.mjs`
|
package/cookies.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import Cookies from "js-cookie";
|
|
2
|
+
|
|
3
|
+
import { getServerCookie, setServerCookie } from "./serverCookies";
|
|
4
|
+
|
|
5
|
+
import type { CookieSetOptions } from "./serverCookies";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Retrieves a cookie value by its name, working seamlessly in both client and server environments.
|
|
9
|
+
* This is crucial for applications that need to maintain state or user preferences across page loads
|
|
10
|
+
* and server-side rendering scenarios.
|
|
11
|
+
*
|
|
12
|
+
* @param name - The unique identifier of the cookie you want to retrieve (e.g., 'user_session', 'theme_preference')
|
|
13
|
+
* @returns The value stored in the cookie if it exists, or undefined if the cookie is not found
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // Get user's theme preference
|
|
17
|
+
* const theme = getCookie('theme_preference');
|
|
18
|
+
* if (theme === 'dark') {
|
|
19
|
+
* // Apply dark theme
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
export const getCookie = async (name: string): Promise<string | undefined> => {
|
|
23
|
+
// Check if code is running in browser
|
|
24
|
+
if (typeof window !== "undefined") {
|
|
25
|
+
// Client-side cookie retrieval
|
|
26
|
+
return Cookies.get(name);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Server-side cookie retrieval
|
|
30
|
+
return getServerCookie(name);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sets a cookie with the specified name, value, and optional configuration parameters.
|
|
35
|
+
* Essential for storing user preferences, session tokens, or any client-side state that
|
|
36
|
+
* needs to persist across page reloads or browser sessions.
|
|
37
|
+
*
|
|
38
|
+
* @param name - The unique identifier for the cookie (e.g., 'auth_token', 'language_preference')
|
|
39
|
+
* @param value - The data to be stored in the cookie
|
|
40
|
+
* @param options - Configuration object for the cookie
|
|
41
|
+
* @param options.domain - Specifies which domains can access the cookie
|
|
42
|
+
* @param options.expires - When the cookie should expire, either as a Date object or days from now
|
|
43
|
+
* @param options.secure - If true, cookie will only be transmitted over HTTPS
|
|
44
|
+
* @param options.sameSite - Controls how the cookie behaves with cross-site requests
|
|
45
|
+
* @param options.path - The path on the server the cookie is valid for
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Set a session cookie that expires in 7 days
|
|
49
|
+
* setCookie('session_id', 'abc123', {
|
|
50
|
+
* expires: 7,
|
|
51
|
+
* secure: true,
|
|
52
|
+
* sameSite: 'strict'
|
|
53
|
+
* });
|
|
54
|
+
*/
|
|
55
|
+
export const setCookie = async (name: string, value: string, options?: CookieSetOptions) => {
|
|
56
|
+
if (typeof window !== "undefined") {
|
|
57
|
+
// Set cookie on the client side without hitting the server
|
|
58
|
+
Cookies.set(name, value, options);
|
|
59
|
+
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Set cookie on the server side
|
|
64
|
+
await setServerCookie(name, value, options);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Removes a cookie from the browser, effectively logging out users or clearing stored preferences.
|
|
69
|
+
* Useful for scenarios like user logout, clearing cached data, or resetting user preferences.
|
|
70
|
+
*
|
|
71
|
+
* @param name - The name of the cookie to remove (e.g., 'auth_token', 'user_session')
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* // Clear user session during logout
|
|
75
|
+
* removeCookie('auth_token');
|
|
76
|
+
* removeCookie('user_preferences');
|
|
77
|
+
*/
|
|
78
|
+
export const removeCookie = async (name: string) => {
|
|
79
|
+
if (typeof window !== "undefined") {
|
|
80
|
+
// Remove cookie on the client side without hitting the server
|
|
81
|
+
return Cookies.remove(name);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Remove cookie on the server side
|
|
85
|
+
return setCookie(name, "", { expires: new Date(0) });
|
|
86
|
+
};
|
package/grafastExchange.ts
CHANGED
|
@@ -6,9 +6,18 @@ import {
|
|
|
6
6
|
type OperationResult,
|
|
7
7
|
} from "urql";
|
|
8
8
|
import { filter, fromPromise, mergeMap, pipe } from "wonka";
|
|
9
|
+
import type { App, H3Event } from "h3";
|
|
10
|
+
import {
|
|
11
|
+
getQuery,
|
|
12
|
+
getRequestHeaders,
|
|
13
|
+
getRequestProtocol,
|
|
14
|
+
readRawBody,
|
|
15
|
+
} from "h3";
|
|
9
16
|
|
|
10
17
|
import type { Maybe } from "grafast";
|
|
11
18
|
import type { PostGraphileInstance } from "postgraphile";
|
|
19
|
+
import { getEvent, getWebRequest } from "vinxi/http";
|
|
20
|
+
import { GrafservBodyBuffer, normalizeRequest, processHeaders, RequestDigest } from "postgraphile/grafserv";
|
|
12
21
|
|
|
13
22
|
export const grafastExchange = (pgl: PostGraphileInstance): Exchange => {
|
|
14
23
|
return () => (ops$) => {
|
|
@@ -16,13 +25,49 @@ export const grafastExchange = (pgl: PostGraphileInstance): Exchange => {
|
|
|
16
25
|
ops$,
|
|
17
26
|
|
|
18
27
|
// Skip anything that's not a query since that's all we can run
|
|
19
|
-
filter((operation) => operation.kind === "query"),
|
|
28
|
+
filter((operation) => operation.kind === "query" || operation.kind === "mutation"),
|
|
20
29
|
|
|
21
30
|
mergeMap((operation) => fromPromise(runGrafastQuery(pgl, operation))),
|
|
22
31
|
);
|
|
23
32
|
};
|
|
24
33
|
};
|
|
25
34
|
|
|
35
|
+
/* This is a direct copy from: https://github.com/graphile/crystal/blob/main/grafast/grafserv/src/servers/h3/v1/index.ts#L24 */
|
|
36
|
+
function getDigest(event: H3Event): RequestDigest {
|
|
37
|
+
const req = event.node.req;
|
|
38
|
+
const res = event.node.res;
|
|
39
|
+
return {
|
|
40
|
+
httpVersionMajor: req.httpVersionMajor,
|
|
41
|
+
httpVersionMinor: req.httpVersionMinor,
|
|
42
|
+
isSecure: getRequestProtocol(event) === "https",
|
|
43
|
+
method: event.method,
|
|
44
|
+
path: event.path,
|
|
45
|
+
headers: processHeaders(getRequestHeaders(event)),
|
|
46
|
+
getQueryParams() {
|
|
47
|
+
return getQuery(event) as Record<string, string | string[]>;
|
|
48
|
+
},
|
|
49
|
+
async getBody() {
|
|
50
|
+
const buffer = await readRawBody(event, false);
|
|
51
|
+
if (!buffer) {
|
|
52
|
+
throw new Error("Failed to retrieve body from h3");
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
type: "buffer",
|
|
56
|
+
buffer,
|
|
57
|
+
} as GrafservBodyBuffer;
|
|
58
|
+
},
|
|
59
|
+
requestContext: {
|
|
60
|
+
h3v1: {
|
|
61
|
+
event,
|
|
62
|
+
},
|
|
63
|
+
node: {
|
|
64
|
+
req,
|
|
65
|
+
res,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
26
71
|
/**
|
|
27
72
|
* Run the URQL query with Grafast, without making an HTTP request to ourselves (which is unnecessary overhead).
|
|
28
73
|
*
|
|
@@ -37,16 +82,25 @@ const runGrafastQuery = async (
|
|
|
37
82
|
try {
|
|
38
83
|
const { variables: variableValues, query: document } = operation;
|
|
39
84
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
85
|
+
const { schema, resolvedPreset } = await pgl.getSchemaResult();
|
|
86
|
+
|
|
87
|
+
// Request/H3 info so we can run with it
|
|
88
|
+
const event = await getEvent();
|
|
89
|
+
const http = normalizeRequest(getDigest(event));
|
|
90
|
+
|
|
91
|
+
// Add the Graphile context to our args so Grafast can run
|
|
92
|
+
const args = await hookArgs({
|
|
93
|
+
schema,
|
|
43
94
|
document,
|
|
44
95
|
variableValues: variableValues as Maybe<{
|
|
45
96
|
readonly [variable: string]: unknown;
|
|
46
97
|
}>,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
98
|
+
resolvedPreset,
|
|
99
|
+
requestContext: {
|
|
100
|
+
...http.requestContext,
|
|
101
|
+
http,
|
|
102
|
+
}
|
|
103
|
+
});
|
|
50
104
|
|
|
51
105
|
// Run the query with Grafast, to get the result from PostgreSQL
|
|
52
106
|
const result = await execute(args);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@carvajalconsultants/headstart",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Library to assist in integrating PostGraphile with Tanstack Start and URQL.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Miguel Carvajal <omar@carvajalonline.com>",
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
"main": "server.ts",
|
|
10
10
|
"files": [
|
|
11
11
|
"client.ts",
|
|
12
|
+
"cookies.ts",
|
|
12
13
|
"grafastExchange.ts",
|
|
13
14
|
"graphQLRouteHandler.ts",
|
|
14
|
-
"server.ts"
|
|
15
|
+
"server.ts",
|
|
16
|
+
"serverCookies.ts"
|
|
15
17
|
],
|
|
16
18
|
"scripts": {
|
|
17
19
|
"lint": "biome check --write"
|
package/serverCookies.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
export interface CookieSetOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.3|Domain Set-Cookie attribute}. By default, no
|
|
4
|
+
* domain is set, and most clients will consider the cookie to apply to only
|
|
5
|
+
* the current domain.
|
|
6
|
+
*/
|
|
7
|
+
domain?: string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Specifies a function that will be used to encode a cookie's value. Since
|
|
10
|
+
* value of a cookie has a limited character set (and must be a simple
|
|
11
|
+
* string), this function can be used to encode a value into a string suited
|
|
12
|
+
* for a cookie's value.
|
|
13
|
+
*
|
|
14
|
+
* The default function is the global `encodeURIComponent`, which will
|
|
15
|
+
* encode a JavaScript string into UTF-8 byte sequences and then URL-encode
|
|
16
|
+
* any that fall outside of the cookie range.
|
|
17
|
+
*/
|
|
18
|
+
encode?(value: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* Specifies the `Date` object to be the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.1|`Expires` `Set-Cookie` attribute}. By default,
|
|
21
|
+
* no expiration is set, and most clients will consider this a "non-persistent cookie" and will delete
|
|
22
|
+
* it on a condition like exiting a web browser application.
|
|
23
|
+
*
|
|
24
|
+
* *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification}
|
|
25
|
+
* states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is
|
|
26
|
+
* possible not all clients by obey this, so if both are set, they should
|
|
27
|
+
* point to the same date and time.
|
|
28
|
+
*/
|
|
29
|
+
expires?: Date | number | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.6|`HttpOnly` `Set-Cookie` attribute}.
|
|
32
|
+
* When truthy, the `HttpOnly` attribute is set, otherwise it is not. By
|
|
33
|
+
* default, the `HttpOnly` attribute is not set.
|
|
34
|
+
*
|
|
35
|
+
* *Note* be careful when setting this to true, as compliant clients will
|
|
36
|
+
* not allow client-side JavaScript to see the cookie in `document.cookie`.
|
|
37
|
+
*/
|
|
38
|
+
httpOnly?: boolean | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Specifies the number (in seconds) to be the value for the `Max-Age`
|
|
41
|
+
* `Set-Cookie` attribute. The given number will be converted to an integer
|
|
42
|
+
* by rounding down. By default, no maximum age is set.
|
|
43
|
+
*
|
|
44
|
+
* *Note* the {@link https://tools.ietf.org/html/rfc6265#section-5.3|cookie storage model specification}
|
|
45
|
+
* states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but it is
|
|
46
|
+
* possible not all clients by obey this, so if both are set, they should
|
|
47
|
+
* point to the same date and time.
|
|
48
|
+
*/
|
|
49
|
+
maxAge?: number | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* Specifies the value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.4|`Path` `Set-Cookie` attribute}.
|
|
52
|
+
* By default, the path is considered the "default path".
|
|
53
|
+
*/
|
|
54
|
+
path?: string | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Specifies the `string` to be the value for the [`Priority` `Set-Cookie` attribute][rfc-west-cookie-priority-00-4.1].
|
|
57
|
+
*
|
|
58
|
+
* - `'low'` will set the `Priority` attribute to `Low`.
|
|
59
|
+
* - `'medium'` will set the `Priority` attribute to `Medium`, the default priority when not set.
|
|
60
|
+
* - `'high'` will set the `Priority` attribute to `High`.
|
|
61
|
+
*
|
|
62
|
+
* More information about the different priority levels can be found in
|
|
63
|
+
* [the specification][rfc-west-cookie-priority-00-4.1].
|
|
64
|
+
*
|
|
65
|
+
* **note** This is an attribute that has not yet been fully standardized, and may change in the future.
|
|
66
|
+
* This also means many clients may ignore this attribute until they understand it.
|
|
67
|
+
*/
|
|
68
|
+
priority?: "low" | "medium" | "high" | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Specifies the boolean or string to be the value for the {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|`SameSite` `Set-Cookie` attribute}.
|
|
71
|
+
*
|
|
72
|
+
* - `true` will set the `SameSite` attribute to `Strict` for strict same
|
|
73
|
+
* site enforcement.
|
|
74
|
+
* - `false` will not set the `SameSite` attribute.
|
|
75
|
+
* - `'lax'` will set the `SameSite` attribute to Lax for lax same site
|
|
76
|
+
* enforcement.
|
|
77
|
+
* - `'strict'` will set the `SameSite` attribute to Strict for strict same
|
|
78
|
+
* site enforcement.
|
|
79
|
+
* - `'none'` will set the SameSite attribute to None for an explicit
|
|
80
|
+
* cross-site cookie.
|
|
81
|
+
*
|
|
82
|
+
* More information about the different enforcement levels can be found in {@link https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.2.7|the specification}.
|
|
83
|
+
*
|
|
84
|
+
* *note* This is an attribute that has not yet been fully standardized, and may change in the future. This also means many clients may ignore this attribute until they understand it.
|
|
85
|
+
*/
|
|
86
|
+
sameSite?: "lax" | "strict" | "none" | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Specifies the boolean value for the {@link https://tools.ietf.org/html/rfc6265#section-5.2.5|`Secure` `Set-Cookie` attribute}. When truthy, the
|
|
89
|
+
* `Secure` attribute is set, otherwise it is not. By default, the `Secure` attribute is not set.
|
|
90
|
+
*
|
|
91
|
+
* *Note* be careful when setting this to `true`, as compliant clients will
|
|
92
|
+
* not send the cookie back to the server in the future if the browser does
|
|
93
|
+
* not have an HTTPS connection.
|
|
94
|
+
*/
|
|
95
|
+
secure?: boolean | undefined;
|
|
96
|
+
/**
|
|
97
|
+
* Specifies the `boolean` value for the [`Partitioned` `Set-Cookie`](https://datatracker.ietf.org/doc/html/draft-cutler-httpbis-partitioned-cookies#section-2.1)
|
|
98
|
+
* attribute. When truthy, the `Partitioned` attribute is set, otherwise it is not. By default, the
|
|
99
|
+
* `Partitioned` attribute is not set.
|
|
100
|
+
*
|
|
101
|
+
* **note** This is an attribute that has not yet been fully standardized, and may change in the future.
|
|
102
|
+
* This also means many clients may ignore this attribute until they understand it.
|
|
103
|
+
*
|
|
104
|
+
* More information can be found in the [proposal](https://github.com/privacycg/CHIPS).
|
|
105
|
+
*/
|
|
106
|
+
partitioned?: boolean;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Retrieves a cookie value during server-side rendering (SSR)
|
|
111
|
+
* This is useful when you need to access cookies that were set by the server
|
|
112
|
+
* before the page is sent to the client, such as authentication tokens or user preferences
|
|
113
|
+
*
|
|
114
|
+
* @param name - The name of the cookie to retrieve
|
|
115
|
+
* @returns The cookie value if running on the server, undefined if on the client
|
|
116
|
+
*/
|
|
117
|
+
export const getServerCookie = async (name: string) => {
|
|
118
|
+
// Only execute this logic during server-side rendering: https://v3.vitejs.dev/guide/ssr.html#conditional-logic
|
|
119
|
+
if (import.meta.env.SSR) {
|
|
120
|
+
// Dynamically import the cookie getter to avoid loading it on the client bundle
|
|
121
|
+
const { getCookie } = await import("@tanstack/react-start-server");
|
|
122
|
+
|
|
123
|
+
return getCookie(name);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return undefined;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Sets a cookie during server-side rendering (SSR)
|
|
131
|
+
* This is essential for scenarios where you need to set cookies before the initial page load,
|
|
132
|
+
* such as storing session tokens, user preferences, or other server-determined values
|
|
133
|
+
*
|
|
134
|
+
* @param name - The name of the cookie to set
|
|
135
|
+
* @param value - The value to store in the cookie
|
|
136
|
+
* @param options - Cookie configuration options
|
|
137
|
+
* @param options.expires - Expiration date or time in days from now
|
|
138
|
+
* @param options.path - Path where the cookie is valid
|
|
139
|
+
* @param options.domain - Domain where the cookie is valid
|
|
140
|
+
* @param options.secure - Whether the cookie should only be transmitted over HTTPS
|
|
141
|
+
* @param options.httpOnly - Whether the cookie should be inaccessible to JavaScript
|
|
142
|
+
* @param options.sameSite - Controls how the cookie behaves with cross-site requests
|
|
143
|
+
*/
|
|
144
|
+
export const setServerCookie = async (name: string, value: string, options?: CookieSetOptions) => {
|
|
145
|
+
// Only execute this logic during server-side rendering: https://v3.vitejs.dev/guide/ssr.html#conditional-logic
|
|
146
|
+
if (import.meta.env.SSR) {
|
|
147
|
+
// Dynamically import the cookie setter to avoid loading it on the client bundle
|
|
148
|
+
const { setCookie: setStartCookie } = await import("@tanstack/react-start-server");
|
|
149
|
+
|
|
150
|
+
// Set the cookie with provided options, converting numeric expiry to actual date
|
|
151
|
+
// This handles both relative (days from now) and absolute date expiration times
|
|
152
|
+
setStartCookie(name, value, {
|
|
153
|
+
...options,
|
|
154
|
+
expires:
|
|
155
|
+
typeof options?.expires === "number"
|
|
156
|
+
? new Date(Date.now() + options.expires * 864e5) // Convert days to milliseconds (864e5 = 24 * 60 * 60 * 1000)
|
|
157
|
+
: options?.expires,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|