@devmoods/express-extras 0.50.0 → 0.52.0
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/auth/passwordless.d.ts +1 -1
- package/dist/auth/passwordless.js +19 -2
- package/dist/auth/types.d.ts +1 -1
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +18 -8
- package/dist/cache.js +8 -5
- package/dist/channels.d.ts +8 -0
- package/dist/channels.js +66 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -0
- package/dist/context.d.ts +2 -0
- package/dist/context.js +1 -1
- package/dist/cookie-consent.d.ts +3 -0
- package/dist/cookie-consent.js +33 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/vite/defineViteConfig.js +2 -2
- package/dist/vite/ssr.d.ts +3 -3
- package/dist/vite/ssr.js +16 -5
- package/dist/vite/types.d.ts +5 -5
- package/dist/vite/types.js +1 -1
- package/package.json +15 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AnyUser, type AuthProvider } from './types.js';
|
|
2
2
|
export interface PasswordlessAuthProviderOptions<UserModel = AnyUser> {
|
|
3
|
-
getUserByEmail(email: string): Promise<UserModel>;
|
|
3
|
+
getUserByEmail(email: string): Promise<UserModel | null>;
|
|
4
4
|
onLoginRequest(user: UserModel, temporaryToken: string): Promise<void>;
|
|
5
5
|
}
|
|
6
6
|
export declare function PasswordlessAuthProvider<UserModel extends AnyUser>(options: PasswordlessAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
@@ -3,9 +3,21 @@ import {} from './types.js';
|
|
|
3
3
|
import { Unauthorized } from '../errors.js';
|
|
4
4
|
import { getLogger } from '../logger.js';
|
|
5
5
|
import { route } from '../middleware.js';
|
|
6
|
+
import { DoesNotExistError } from '../postgres.js';
|
|
6
7
|
import { validateBody } from '../validate.js';
|
|
7
8
|
const logger = getLogger('auth');
|
|
8
9
|
export function PasswordlessAuthProvider(options) {
|
|
10
|
+
const getUserOrNull = async (email) => {
|
|
11
|
+
try {
|
|
12
|
+
return await options.getUserByEmail(email);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error instanceof DoesNotExistError) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
9
21
|
return {
|
|
10
22
|
setupRoutes(router, context) {
|
|
11
23
|
router.post('/passwordless/login/:token', route(async (req, res) => {
|
|
@@ -16,10 +28,11 @@ export function PasswordlessAuthProvider(options) {
|
|
|
16
28
|
await context.verifyTemporaryToken(temporaryToken, user);
|
|
17
29
|
}
|
|
18
30
|
catch {
|
|
31
|
+
logger.warn('passwordless: invalid temp token', { uid: user.id });
|
|
19
32
|
throw Unauthorized('Invalid token');
|
|
20
33
|
}
|
|
21
34
|
const accessToken = await context.login(req, res, user);
|
|
22
|
-
logger.info('passwordless login');
|
|
35
|
+
logger.info('passwordless login', { uid: user.id });
|
|
23
36
|
if (req.accepts('html')) {
|
|
24
37
|
return res.redirect('/');
|
|
25
38
|
}
|
|
@@ -36,7 +49,11 @@ export function PasswordlessAuthProvider(options) {
|
|
|
36
49
|
required: ['email'],
|
|
37
50
|
}), route(async (req) => {
|
|
38
51
|
const { email } = req.body;
|
|
39
|
-
const user = await
|
|
52
|
+
const user = await getUserOrNull(email);
|
|
53
|
+
if (!user) {
|
|
54
|
+
logger.warn('passwordless: user not found', { email });
|
|
55
|
+
return 202;
|
|
56
|
+
}
|
|
40
57
|
const token = await context.createTemporaryToken(user);
|
|
41
58
|
await options.onLoginRequest(user, token);
|
|
42
59
|
logger.info('passwordless login requested', { email });
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface AuthProviderContext<UserModel extends AnyUser> {
|
|
|
38
38
|
verifyTemporaryToken(token: string, user: UserModel): Promise<void>;
|
|
39
39
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
40
40
|
middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
|
|
41
|
-
login: (req: Request, res: Response, user: UserModel) => Promise<AccessToken>;
|
|
41
|
+
login: (req: Request, res: Response, user: UserModel, isRefresh?: boolean) => Promise<AccessToken>;
|
|
42
42
|
getUserById(id: string): Promise<UserModel>;
|
|
43
43
|
jitter(duration?: number): Promise<void>;
|
|
44
44
|
}
|
package/dist/auth.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface CreateAuth<UserModel extends AnyUser> {
|
|
|
9
9
|
createRouter: () => Router;
|
|
10
10
|
useCurrentUser: () => UserModel | undefined;
|
|
11
11
|
middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
|
|
12
|
+
before: () => RequestHandler;
|
|
13
|
+
isAuthorized: (fn?: (user: UserModel) => boolean) => RequestHandler;
|
|
12
14
|
createTemporaryToken(user: UserModel): Promise<string>;
|
|
13
15
|
verifyTemporaryToken(token: string, user: UserModel): Promise<void>;
|
|
14
16
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
package/dist/auth.js
CHANGED
|
@@ -16,7 +16,7 @@ export * from './auth/emailAndPassword.js';
|
|
|
16
16
|
export * from './auth/passwordless.js';
|
|
17
17
|
export * from './auth/token.js';
|
|
18
18
|
export class RedisTokenStorage extends TokenStorage {
|
|
19
|
-
constructor(redis, keyPrefix =
|
|
19
|
+
constructor(redis, keyPrefix = `${config.value.REDIS_KEY_PREFIX}auth:`) {
|
|
20
20
|
super();
|
|
21
21
|
this.redis = redis;
|
|
22
22
|
this.keyPrefix = keyPrefix;
|
|
@@ -37,17 +37,19 @@ const sha256 = (value) => crypto.createHash('sha256').update(value).digest('hex'
|
|
|
37
37
|
export function createAuth({ getUserById, filterUser, tokenStorage, providers, loginUrl = '/', onUserLoggedIn, sessionTokenVersion = '1', }) {
|
|
38
38
|
const useCurrentUser = () => useContext('currentUser');
|
|
39
39
|
const setCurrentUser = (user) => setContext('currentUser', user);
|
|
40
|
-
const login = async (req, res, user) => {
|
|
40
|
+
const login = async (req, res, user, isRefresh = false) => {
|
|
41
41
|
const accessToken = createAccessToken({ uid: user.id }, sessionTokenVersion);
|
|
42
42
|
const sessionId = sha256(accessToken.token);
|
|
43
43
|
await tokenStorage.set(sessionId, String(user.id), accessToken.expiresIn);
|
|
44
44
|
setAccessTokenCookie(res, accessToken.token);
|
|
45
45
|
setCurrentUser(user);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
if (!isRefresh) {
|
|
47
|
+
await onUserLoggedIn?.(user, {
|
|
48
|
+
ip: req.ip ?? '',
|
|
49
|
+
userAgent: req.headers['user-agent'] ?? '',
|
|
50
|
+
sessionId,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
51
53
|
return accessToken;
|
|
52
54
|
};
|
|
53
55
|
const logout = async (res, sessionId) => {
|
|
@@ -106,7 +108,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
106
108
|
setAccessTokenCookie(res, null);
|
|
107
109
|
throw Unauthorized();
|
|
108
110
|
}
|
|
109
|
-
const { token } = await providerContext.login(req, res, user);
|
|
111
|
+
const { token } = await providerContext.login(req, res, user, true);
|
|
110
112
|
logger.info('refreshed token');
|
|
111
113
|
return { token, user: filterUser(user) };
|
|
112
114
|
}));
|
|
@@ -153,9 +155,15 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
153
155
|
await logout(res, token);
|
|
154
156
|
throw new InvalidToken('User not found');
|
|
155
157
|
}
|
|
158
|
+
const tokenLifetimeMs = decoded.exp * 1000 - decoded.iat * 1000;
|
|
159
|
+
const remainingMs = decoded.exp * 1000 - Date.now();
|
|
160
|
+
const shouldRefresh = remainingMs < tokenLifetimeMs * 0.25;
|
|
156
161
|
if (options.isAuthorized && !(await options.isAuthorized(user))) {
|
|
157
162
|
throw new PermissionDenied('Permission denied');
|
|
158
163
|
}
|
|
164
|
+
if (shouldRefresh) {
|
|
165
|
+
await providerContext.login(req, res, user, true);
|
|
166
|
+
}
|
|
159
167
|
setCurrentUser(user);
|
|
160
168
|
next();
|
|
161
169
|
}
|
|
@@ -225,6 +233,8 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
225
233
|
await tokenStorage.invalidate(token);
|
|
226
234
|
}
|
|
227
235
|
return {
|
|
236
|
+
before: () => middleware(false),
|
|
237
|
+
isAuthorized: (fn) => middleware({ isAuthorized: fn || (() => true) }),
|
|
228
238
|
createRouter,
|
|
229
239
|
useCurrentUser,
|
|
230
240
|
middleware,
|
package/dist/cache.js
CHANGED
|
@@ -1,21 +1,24 @@
|
|
|
1
|
+
import { config } from './config.js';
|
|
1
2
|
import {} from './redis.js';
|
|
2
3
|
export function createRedisCacheAdapter(redis, options = {}) {
|
|
3
|
-
const { keyPrefix =
|
|
4
|
-
const
|
|
4
|
+
const { keyPrefix = `${config.value.REDIS_KEY_PREFIX}cache:` } = options;
|
|
5
|
+
const withKeyPrefix = (key) => `${keyPrefix}${key}`;
|
|
5
6
|
return {
|
|
6
7
|
async get(key) {
|
|
7
|
-
const value = await redis.get(
|
|
8
|
+
const value = await redis.get(withKeyPrefix(key));
|
|
8
9
|
if (value == null) {
|
|
9
10
|
return null;
|
|
10
11
|
}
|
|
11
12
|
return JSON.parse(value);
|
|
12
13
|
},
|
|
13
14
|
async set(key, value, expires) {
|
|
14
|
-
await redis.set(
|
|
15
|
+
await redis.set(withKeyPrefix(key), JSON.stringify(value), {
|
|
16
|
+
PX: expires,
|
|
17
|
+
});
|
|
15
18
|
},
|
|
16
19
|
async clear(key) {
|
|
17
20
|
if (key) {
|
|
18
|
-
await redis.del(
|
|
21
|
+
await redis.del(withKeyPrefix(key));
|
|
19
22
|
}
|
|
20
23
|
const keys = await redis.keys(`${keyPrefix}*`);
|
|
21
24
|
for (const existingKey of keys) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Request } from 'express';
|
|
2
|
+
import { type RedisClient } from './redis.js';
|
|
3
|
+
export declare function createEventStream<Channels extends string>(redis: RedisClient): {
|
|
4
|
+
sse: ({ channels, }: {
|
|
5
|
+
channels: (req: Request) => string[] | Promise<string[]>;
|
|
6
|
+
}) => import("express").RequestHandler<Record<string, string>, Record<string, any>, unknown, Record<string, any>, Record<string, any>>;
|
|
7
|
+
publish: (channel: Channels, message: unknown) => Promise<number>;
|
|
8
|
+
};
|
package/dist/channels.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {} from 'express';
|
|
2
|
+
import { config } from './config.js';
|
|
3
|
+
import { requestContext } from './context.js';
|
|
4
|
+
import { getLogger } from './logger.js';
|
|
5
|
+
import { route } from './middleware.js';
|
|
6
|
+
import {} from './redis.js';
|
|
7
|
+
const logger = getLogger();
|
|
8
|
+
export function createEventStream(redis) {
|
|
9
|
+
const keyPrefix = `${config.value.REDIS_KEY_PREFIX}events:`;
|
|
10
|
+
const withKeyPrefix = (key) => `${keyPrefix}${key}`;
|
|
11
|
+
const sse = ({ channels, }) => {
|
|
12
|
+
return route(async (req, res) => {
|
|
13
|
+
const headers = {
|
|
14
|
+
'Content-Type': 'text/event-stream',
|
|
15
|
+
'Cache-Control': 'no-cache',
|
|
16
|
+
Connection: 'keep-alive',
|
|
17
|
+
};
|
|
18
|
+
res.writeHead(200, headers);
|
|
19
|
+
res.write('retry: 15000\n');
|
|
20
|
+
const heartbeat = setInterval(() => {
|
|
21
|
+
try {
|
|
22
|
+
if (!res.writableEnded) {
|
|
23
|
+
res.write('event: heartbeat\n');
|
|
24
|
+
res.write('data: ping\n\n');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
logger.error(error);
|
|
29
|
+
}
|
|
30
|
+
}, 15000);
|
|
31
|
+
const store = requestContext.getStore();
|
|
32
|
+
const listener = (value) => {
|
|
33
|
+
try {
|
|
34
|
+
requestContext.run(store, () => {
|
|
35
|
+
if (!res.writableEnded) {
|
|
36
|
+
res.write('event: data\n');
|
|
37
|
+
res.write(`data: ${JSON.stringify(value)}\n\n`);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
logger.error(error);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const subscriber = redis.duplicate();
|
|
46
|
+
await subscriber.connect();
|
|
47
|
+
const _channels = await channels(req);
|
|
48
|
+
await subscriber.subscribe(_channels.map(withKeyPrefix), (message) => {
|
|
49
|
+
listener(JSON.parse(message));
|
|
50
|
+
});
|
|
51
|
+
logger.info('event stream subscribe', { channels: _channels });
|
|
52
|
+
res.on('close', () => {
|
|
53
|
+
requestContext.run(store, () => {
|
|
54
|
+
logger.info('event stream close');
|
|
55
|
+
clearInterval(heartbeat);
|
|
56
|
+
subscriber.quit();
|
|
57
|
+
res.end();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
const publish = (channel, message) => {
|
|
63
|
+
return redis.publish(withKeyPrefix(channel), JSON.stringify(message));
|
|
64
|
+
};
|
|
65
|
+
return { sse, publish };
|
|
66
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const config: {
|
|
|
7
7
|
AUTH_TEMP_TOKEN_EXPIRES_IN: number;
|
|
8
8
|
AUTH_SECRET: string;
|
|
9
9
|
REDIS_URL: string;
|
|
10
|
+
REDIS_KEY_PREFIX: string;
|
|
10
11
|
POSTGRES_URL: string;
|
|
11
12
|
SENTRY_DSN: string;
|
|
12
13
|
LOG_LEVEL: string;
|
|
@@ -37,6 +38,7 @@ export declare function extendConfig<T extends Record<string, unknown>>(appConfi
|
|
|
37
38
|
AUTH_TEMP_TOKEN_EXPIRES_IN: number;
|
|
38
39
|
AUTH_SECRET: string;
|
|
39
40
|
REDIS_URL: string;
|
|
41
|
+
REDIS_KEY_PREFIX: string;
|
|
40
42
|
POSTGRES_URL: string;
|
|
41
43
|
SENTRY_DSN: string;
|
|
42
44
|
LOG_LEVEL: string;
|
package/dist/config.js
CHANGED
|
@@ -10,6 +10,7 @@ const baseConfig = {
|
|
|
10
10
|
AUTH_TEMP_TOKEN_EXPIRES_IN: readEnv.number('AUTH_TEMP_TOKEN_EXPIRES', 3600),
|
|
11
11
|
AUTH_SECRET: readEnv('AUTH_JWT_SECRET', ''),
|
|
12
12
|
REDIS_URL: readEnv('REDIS_URL', ''),
|
|
13
|
+
REDIS_KEY_PREFIX: readEnv('REDIS_KEY_PREFIX', ''),
|
|
13
14
|
POSTGRES_URL: readEnv('POSTGRES_URL', ''),
|
|
14
15
|
SENTRY_DSN: readEnv('SENTRY_DSN', ''),
|
|
15
16
|
LOG_LEVEL: readEnv('LOG_LEVEL', 'INFO'),
|
package/dist/context.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
2
|
import { type Request, type RequestHandler } from 'express';
|
|
3
|
+
export declare const requestContext: AsyncLocalStorage<Map<string, any>>;
|
|
2
4
|
export interface RequestContextOptions {
|
|
3
5
|
getRequestId?: (req: Request) => string | undefined;
|
|
4
6
|
}
|
package/dist/context.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
2
|
import {} from 'express';
|
|
3
3
|
import { v4 as uuid } from 'uuid';
|
|
4
|
-
const requestContext = new AsyncLocalStorage();
|
|
4
|
+
export const requestContext = new AsyncLocalStorage();
|
|
5
5
|
const REQUEST_ID = 'requestId';
|
|
6
6
|
/**
|
|
7
7
|
* Middleware for setting a `requestId` on the request context.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { getLogger } from './logger.js';
|
|
3
|
+
import { route } from './middleware.js';
|
|
4
|
+
import { validateBody } from './validate.js';
|
|
5
|
+
const logger = getLogger();
|
|
6
|
+
export function cookieConsent({ onConsent, }) {
|
|
7
|
+
const router = Router();
|
|
8
|
+
router.get('/', route(() => {
|
|
9
|
+
return {
|
|
10
|
+
consents: [],
|
|
11
|
+
};
|
|
12
|
+
}));
|
|
13
|
+
router.post('/', validateBody({
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
consents: {
|
|
17
|
+
type: 'array',
|
|
18
|
+
items: { type: 'string' },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}), route(async (req, res) => {
|
|
22
|
+
const { consents } = req.body;
|
|
23
|
+
res.cookie('cookie_consent', consents.join(',') || '1', {
|
|
24
|
+
expires: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000),
|
|
25
|
+
sameSite: 'strict',
|
|
26
|
+
secure: process.env.NODE_ENV === 'production',
|
|
27
|
+
});
|
|
28
|
+
await onConsent?.(consents);
|
|
29
|
+
logger.info('cookie consent');
|
|
30
|
+
return { consents };
|
|
31
|
+
}));
|
|
32
|
+
return router;
|
|
33
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react';
|
|
|
4
4
|
import Sonda from 'sonda/vite';
|
|
5
5
|
import { defineConfig } from 'vite';
|
|
6
6
|
import { readEnv } from '../utils.js';
|
|
7
|
-
export function defineViteConfig({ base = '', reactCompiler = false, ssr = false, analyze =
|
|
7
|
+
export function defineViteConfig({ base = '', reactCompiler = false, ssr = false, analyze = readEnv.boolean('VITE_ANALYZE', false), }) {
|
|
8
8
|
const port = readEnv.number('VITE_PORT', 5173);
|
|
9
9
|
return defineConfig({
|
|
10
10
|
plugins: [
|
|
@@ -28,7 +28,7 @@ export function defineViteConfig({ base = '', reactCompiler = false, ssr = false
|
|
|
28
28
|
build: {
|
|
29
29
|
chunkSizeWarningLimit: 750,
|
|
30
30
|
manifest: true,
|
|
31
|
-
sourcemap:
|
|
31
|
+
sourcemap: analyze,
|
|
32
32
|
// Set via cli arg --outDir for ssr
|
|
33
33
|
outDir: ssr ? undefined : 'dist/client',
|
|
34
34
|
rollupOptions: ssr
|
package/dist/vite/ssr.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export interface
|
|
1
|
+
export interface SsrMiddlewareOptions<InitialData = unknown> {
|
|
2
2
|
root?: string;
|
|
3
|
-
getInitialData?: (req:
|
|
3
|
+
getInitialData?: (req: Request) => Promise<InitialData>;
|
|
4
4
|
useDevServer?: boolean;
|
|
5
5
|
}
|
|
6
|
-
export declare function createSsrMiddleware({ root, getInitialData, useDevServer, }?:
|
|
6
|
+
export declare function createSsrMiddleware<InitialData = unknown>({ root, getInitialData, useDevServer, }?: SsrMiddlewareOptions<InitialData>): Promise<import("compose-middleware").RequestHandler<unknown, unknown, void>>;
|
package/dist/vite/ssr.js
CHANGED
|
@@ -4,8 +4,10 @@ import express, {} from 'express';
|
|
|
4
4
|
import { createServer as createViteServer } from 'vite';
|
|
5
5
|
import {} from './types.js';
|
|
6
6
|
import { config } from '../config.js';
|
|
7
|
+
import { generateCsrfToken } from '../csrf.js';
|
|
7
8
|
import { composeMiddleware, route, useCspNonce } from '../middleware.js';
|
|
8
|
-
|
|
9
|
+
import { createFetchRequest } from '../webapi.js';
|
|
10
|
+
export async function createSsrMiddleware({ root = process.cwd(), getInitialData, useDevServer = config.value.VITE_USE_DEV_SERVER, } = {}) {
|
|
9
11
|
const resolve = (p) => path.resolve(root, p);
|
|
10
12
|
let vite;
|
|
11
13
|
const middlewares = [];
|
|
@@ -31,12 +33,13 @@ export async function createSsrMiddleware({ root = process.cwd(), getInitialData
|
|
|
31
33
|
render = result.render;
|
|
32
34
|
}
|
|
33
35
|
const nonce = useCspNonce();
|
|
34
|
-
const rendered = await render(req,
|
|
36
|
+
const rendered = await render(createFetchRequest(req), {
|
|
35
37
|
useDevServer,
|
|
36
|
-
getInitialData,
|
|
38
|
+
getInitialData: getInitialData || (async () => ({})),
|
|
37
39
|
placeholders: `
|
|
38
40
|
{{ ssr-title }}
|
|
39
41
|
{{ ssr-meta }}
|
|
42
|
+
{{ ssr-fouc }}
|
|
40
43
|
{{ ssr-preload }}
|
|
41
44
|
{{ ssr-js }}
|
|
42
45
|
{{ ssr-css }}
|
|
@@ -49,7 +52,8 @@ export async function createSsrMiddleware({ root = process.cwd(), getInitialData
|
|
|
49
52
|
window.$RefreshSig$ = () => (type) => type
|
|
50
53
|
window.__vite_plugin_react_preamble_installed__ = true
|
|
51
54
|
`,
|
|
52
|
-
publicUrl: config.value.PUBLIC_URL,
|
|
55
|
+
publicUrl: new URL(config.value.PUBLIC_URL),
|
|
56
|
+
csrfToken: generateCsrfToken(req, res),
|
|
53
57
|
});
|
|
54
58
|
const readManifest = async () => {
|
|
55
59
|
if (useDevServer) {
|
|
@@ -59,7 +63,14 @@ export async function createSsrMiddleware({ root = process.cwd(), getInitialData
|
|
|
59
63
|
return getAssetLinks(manifestPath, 'src/entry-client.tsx');
|
|
60
64
|
};
|
|
61
65
|
let html = rendered.html;
|
|
62
|
-
const context = Object.assign({
|
|
66
|
+
const context = Object.assign({
|
|
67
|
+
fouc: `
|
|
68
|
+
<style id="fouc">body { visibility: hidden; opacity: 0.0 }</style>
|
|
69
|
+
<script nonce="${nonce}">
|
|
70
|
+
setTimeout(() => { document.getElementById('fouc').remove(); }, 2000);
|
|
71
|
+
</script>
|
|
72
|
+
`,
|
|
73
|
+
}, rendered.context, await readManifest());
|
|
63
74
|
const ssrPlaceholders = Object.fromEntries(Object.entries(context).map(([k, v]) => [`ssr-${k}`, v.toString()]));
|
|
64
75
|
// Add nonce to all bare script tags
|
|
65
76
|
html = html.replace(/<script>/g, `<script nonce="${nonce}">`);
|
package/dist/vite/types.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
export interface SsrRenderOptions {
|
|
1
|
+
export interface SsrRenderOptions<InitialData = unknown> {
|
|
3
2
|
useDevServer: boolean;
|
|
4
3
|
placeholders: string;
|
|
5
4
|
nonce: string;
|
|
5
|
+
csrfToken: string;
|
|
6
6
|
preamble: string;
|
|
7
|
-
getInitialData: (req:
|
|
8
|
-
publicUrl:
|
|
7
|
+
getInitialData: (req: Request) => Promise<InitialData>;
|
|
8
|
+
publicUrl: URL;
|
|
9
9
|
}
|
|
10
|
-
export type EntryRenderFn = (req: Request,
|
|
10
|
+
export type EntryRenderFn = (req: Request, options: SsrRenderOptions) => Promise<{
|
|
11
11
|
html: string;
|
|
12
12
|
context: any;
|
|
13
13
|
}>;
|
package/dist/vite/types.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -46,51 +46,51 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@devmoods/eslint-config": "^2.6.0",
|
|
48
48
|
"@types/koa-compose": "^3.2.8",
|
|
49
|
-
"@types/node": "22.
|
|
49
|
+
"@types/node": "22.13.0",
|
|
50
50
|
"@types/supertest": "^6.0.2",
|
|
51
51
|
"@vitejs/plugin-react": "^4.3.4",
|
|
52
|
-
"@vitest/coverage-v8": "^3.0.
|
|
52
|
+
"@vitest/coverage-v8": "^3.0.4",
|
|
53
|
+
"arktype": "^2.0.4",
|
|
53
54
|
"cookiejar": "^2.1.4",
|
|
54
|
-
"eslint": "^9.
|
|
55
|
+
"eslint": "^9.19.0",
|
|
55
56
|
"faktory-worker": "^4.6.0",
|
|
56
57
|
"prettier": "3.4.2",
|
|
57
58
|
"supertest": "7.0.0",
|
|
58
59
|
"ts-node": "10.9.2",
|
|
59
60
|
"tsx": "^4.19.2",
|
|
60
61
|
"typescript": "5.7.3",
|
|
61
|
-
"vite": "^6.0.
|
|
62
|
-
"vitest": "^3.0.
|
|
62
|
+
"vite": "^6.0.11",
|
|
63
|
+
"vitest": "^3.0.4",
|
|
63
64
|
"vitest-fail-on-console": "^0.7.1"
|
|
64
65
|
},
|
|
65
66
|
"dependencies": {
|
|
66
67
|
"@devmoods/fetch": "^4.0.1",
|
|
67
|
-
"@sentry/node": "^8.
|
|
68
|
-
"@sentry/profiling-node": "^8.
|
|
68
|
+
"@sentry/node": "^8.53.0",
|
|
69
|
+
"@sentry/profiling-node": "^8.53.0",
|
|
69
70
|
"@types/bcrypt": "^5.0.2",
|
|
70
71
|
"@types/cookie-parser": "^1.4.8",
|
|
71
72
|
"@types/express": "^5.0.0",
|
|
72
|
-
"@types/jsonwebtoken": "^9.0.
|
|
73
|
+
"@types/jsonwebtoken": "^9.0.8",
|
|
73
74
|
"@types/nodemailer": "^6.4.17",
|
|
74
|
-
"@types/pg": "^8.11.
|
|
75
|
+
"@types/pg": "^8.11.11",
|
|
75
76
|
"@types/superagent": "^8.1.9",
|
|
76
77
|
"@types/uuid": "^10.0.0",
|
|
77
78
|
"ajv": "^8.17.1",
|
|
78
79
|
"argon2": "^0.41.1",
|
|
79
|
-
"arktype": "^2.0.0",
|
|
80
80
|
"bcrypt": "^5.1.1",
|
|
81
81
|
"chalk": "^5.4.1",
|
|
82
|
-
"commander": "^13.
|
|
82
|
+
"commander": "^13.1.0",
|
|
83
83
|
"compose-middleware": "^5.0.1",
|
|
84
84
|
"cookie-parser": "^1.4.7",
|
|
85
85
|
"dotenv": "^16.4.7",
|
|
86
86
|
"express": "^4.21.2",
|
|
87
87
|
"helmet": "^8.0.0",
|
|
88
88
|
"jsonwebtoken": "^9.0.2",
|
|
89
|
-
"nodemailer": "^6.
|
|
89
|
+
"nodemailer": "^6.10.0",
|
|
90
90
|
"pg": "^8.13.1",
|
|
91
91
|
"redis": "^4.7.0",
|
|
92
92
|
"sonda": "^0.7.1",
|
|
93
|
-
"sql-formatter": "^15.4.
|
|
93
|
+
"sql-formatter": "^15.4.10",
|
|
94
94
|
"uuid": "^11.0.5"
|
|
95
95
|
},
|
|
96
96
|
"prettier": {
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
},
|
|
99
99
|
"peerDependencies": {
|
|
100
100
|
"@vitejs/plugin-react": ">=4.3.4",
|
|
101
|
+
"arktype": ">=2.0.0",
|
|
101
102
|
"faktory-worker": ">=4.5.1",
|
|
102
103
|
"vite": ">=6.0.5"
|
|
103
104
|
},
|