@ovineko/spa-guard-fastify 0.0.3-alpha-0 → 0.0.4
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 +27 -71
- package/dist/index.d.ts +42 -38
- package/dist/index.js +105 -108
- package/package.json +3 -6
package/README.md
CHANGED
|
@@ -7,93 +7,49 @@ Fastify plugin for spa-guard beacon endpoint and HTML cache handler with ETag/30
|
|
|
7
7
|
|
|
8
8
|
## Install
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
npm install @ovineko/spa-guard-fastify @ovineko/spa-guard @ovineko/spa-guard-node fastify fastify-plugin
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Usage
|
|
10
|
+
**pnpm** (recommended):
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Register the plugin to receive beacon data posted by the spa-guard client runtime:
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
import Fastify from "fastify";
|
|
22
|
-
import { fastifySPAGuard } from "@ovineko/spa-guard-fastify";
|
|
23
|
-
|
|
24
|
-
const app = Fastify();
|
|
25
|
-
|
|
26
|
-
app.addContentTypeParser("text/plain", { parseAs: "string" }, (_req, body, done) => {
|
|
27
|
-
done(null, body);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
app.register(fastifySPAGuard, {
|
|
31
|
-
path: "/api/beacon",
|
|
32
|
-
onBeacon: async (beacon, request, reply) => {
|
|
33
|
-
request.log.info({ beacon }, "SPA Guard beacon received");
|
|
34
|
-
// optionally suppress default log
|
|
35
|
-
return { skipDefaultLog: true };
|
|
36
|
-
},
|
|
37
|
-
onUnknownBeacon: async (body, request) => {
|
|
38
|
-
request.log.warn({ body }, "Unknown beacon format");
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
await app.listen({ port: 3000 });
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add @ovineko/spa-guard-fastify @ovineko/spa-guard @ovineko/spa-guard-node fastify fastify-plugin
|
|
43
14
|
```
|
|
44
15
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
Serve your SPA's `index.html` with ETag/304 and compression negotiation:
|
|
16
|
+
**npm**:
|
|
48
17
|
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
import { spaGuardFastifyHandler } from "@ovineko/spa-guard-fastify";
|
|
52
|
-
import { createHtmlCache } from "@ovineko/spa-guard-node";
|
|
53
|
-
|
|
54
|
-
const handlerOptions = {};
|
|
55
|
-
|
|
56
|
-
app.get("/*", async (request, reply) => {
|
|
57
|
-
return spaGuardFastifyHandler(request, reply, {
|
|
58
|
-
...handlerOptions,
|
|
59
|
-
getHtml: () => ({ html: "<html>...</html>" }),
|
|
60
|
-
});
|
|
61
|
-
});
|
|
18
|
+
```bash
|
|
19
|
+
npm install @ovineko/spa-guard-fastify @ovineko/spa-guard @ovineko/spa-guard-node fastify fastify-plugin
|
|
62
20
|
```
|
|
63
21
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
### `fastifySPAGuard` (Fastify plugin)
|
|
67
|
-
|
|
68
|
-
Registers a `POST` route at `options.path` to receive beacon payloads.
|
|
22
|
+
**yarn**:
|
|
69
23
|
|
|
70
|
-
|
|
24
|
+
```bash
|
|
25
|
+
yarn add @ovineko/spa-guard-fastify @ovineko/spa-guard @ovineko/spa-guard-node fastify fastify-plugin
|
|
26
|
+
```
|
|
71
27
|
|
|
72
|
-
|
|
73
|
-
- `onBeacon(beacon, request, reply)` - Called with parsed beacon data. Return `{ skipDefaultLog: true }` to suppress default logging.
|
|
74
|
-
- `onUnknownBeacon(body, request, reply)` - Called when beacon fails schema validation. Return `{ skipDefaultLog: true }` to suppress default warning.
|
|
28
|
+
**bun**:
|
|
75
29
|
|
|
76
|
-
|
|
30
|
+
```bash
|
|
31
|
+
bun add @ovineko/spa-guard-fastify @ovineko/spa-guard @ovineko/spa-guard-node fastify fastify-plugin
|
|
32
|
+
```
|
|
77
33
|
|
|
78
|
-
|
|
34
|
+
**deno**:
|
|
79
35
|
|
|
80
|
-
|
|
36
|
+
```bash
|
|
37
|
+
deno add npm:@ovineko/spa-guard-fastify npm:@ovineko/spa-guard npm:@ovineko/spa-guard-node npm:fastify npm:fastify-plugin
|
|
38
|
+
```
|
|
81
39
|
|
|
82
|
-
|
|
83
|
-
- `getHtml()` - Async factory returning `CreateHtmlCacheOptions`; cache is created lazily on first request
|
|
40
|
+
## Usage
|
|
84
41
|
|
|
85
|
-
|
|
42
|
+
```ts
|
|
43
|
+
import Fastify from "fastify";
|
|
44
|
+
import { fastifySPAGuard } from "@ovineko/spa-guard-fastify";
|
|
86
45
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
- `BeaconError`
|
|
46
|
+
const app = Fastify();
|
|
47
|
+
app.register(fastifySPAGuard, { path: "/api/beacon" });
|
|
48
|
+
```
|
|
91
49
|
|
|
92
|
-
##
|
|
50
|
+
## Documentation
|
|
93
51
|
|
|
94
|
-
|
|
95
|
-
- [@ovineko/spa-guard-node](../node/README.md) - Node.js HTML cache
|
|
96
|
-
- [@ovineko/spa-guard-vite](../vite/README.md) - Vite build plugin
|
|
52
|
+
Full documentation: [ovineko.com/docs/spa-guard/fastify](https://ovineko.com/docs/spa-guard/fastify)
|
|
97
53
|
|
|
98
54
|
## License
|
|
99
55
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,41 +1,45 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { CreateHtmlCacheOptions, HtmlCache } from "@ovineko/spa-guard-node";
|
|
2
|
+
import { BeaconError } from "@ovineko/spa-guard";
|
|
3
|
+
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
|
|
4
|
+
import { BeaconSchema } from "@ovineko/spa-guard/schema";
|
|
5
|
+
|
|
6
|
+
//#region src/index.d.ts
|
|
7
|
+
interface BeaconHandlerResult {
|
|
8
|
+
/**
|
|
9
|
+
* If true, skips default logging behavior
|
|
10
|
+
*/
|
|
11
|
+
skipDefaultLog?: boolean;
|
|
10
12
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
13
|
+
interface FastifySPAGuardOptions {
|
|
14
|
+
/**
|
|
15
|
+
* Custom handler for beacon data
|
|
16
|
+
* @param beacon - Parsed beacon data
|
|
17
|
+
* @param request - Fastify request object
|
|
18
|
+
* @param reply - Fastify reply object
|
|
19
|
+
* @returns Object with options to control default behavior
|
|
20
|
+
*/
|
|
21
|
+
onBeacon?: (beacon: BeaconSchema, request: FastifyRequest, reply: FastifyReply) => BeaconHandlerResult | Promise<BeaconHandlerResult | void> | void;
|
|
22
|
+
/**
|
|
23
|
+
* Custom handler for invalid/unknown beacon data
|
|
24
|
+
* @param body - Raw body data
|
|
25
|
+
* @param request - Fastify request object
|
|
26
|
+
* @param reply - Fastify reply object
|
|
27
|
+
* @returns Object with options to control default behavior
|
|
28
|
+
*/
|
|
29
|
+
onUnknownBeacon?: (body: unknown, request: FastifyRequest, reply: FastifyReply) => BeaconHandlerResult | Promise<BeaconHandlerResult | void> | void;
|
|
30
|
+
/**
|
|
31
|
+
* The route path for the beacon endpoint
|
|
32
|
+
* @example "/api/beacon"
|
|
33
|
+
*/
|
|
34
|
+
path: string;
|
|
33
35
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
declare const fastifySPAGuard: FastifyPluginAsync<FastifySPAGuardOptions>;
|
|
37
|
+
interface SpaGuardHandlerOptions {
|
|
38
|
+
/** @internal Promise used to deduplicate concurrent lazy cache creation */
|
|
39
|
+
_cachePromise?: Promise<HtmlCache>;
|
|
40
|
+
cache?: HtmlCache;
|
|
41
|
+
getHtml?: (() => CreateHtmlCacheOptions) | (() => Promise<CreateHtmlCacheOptions>);
|
|
40
42
|
}
|
|
41
|
-
|
|
43
|
+
declare function spaGuardFastifyHandler(request: FastifyRequest, reply: FastifyReply, options: SpaGuardHandlerOptions): Promise<FastifyReply>;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { BeaconError, BeaconHandlerResult, FastifySPAGuardOptions, SpaGuardHandlerOptions, fastifySPAGuard, spaGuardFastifyHandler };
|
package/dist/index.js
CHANGED
|
@@ -1,120 +1,117 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
1
|
import { createHtmlCache } from "@ovineko/spa-guard-node";
|
|
3
2
|
import fp from "fastify-plugin";
|
|
4
3
|
import { BeaconError } from "@ovineko/spa-guard";
|
|
5
4
|
import { logMessage } from "@ovineko/spa-guard/_internal";
|
|
6
5
|
import { parseBeacon } from "@ovineko/spa-guard/schema/parse";
|
|
7
|
-
|
|
8
|
-
// package.json
|
|
6
|
+
//#region package.json
|
|
9
7
|
var name = "@ovineko/spa-guard-fastify";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/index.ts
|
|
10
|
+
const parseStringBody = (body) => {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(body);
|
|
13
|
+
} catch {
|
|
14
|
+
return body;
|
|
15
|
+
}
|
|
18
16
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
if (options.onBeacon) {
|
|
43
|
-
const result = await options.onBeacon(beacon, request, reply);
|
|
44
|
-
if (!result?.skipDefaultLog) {
|
|
45
|
-
request.log.info(logPayload, logMessage("Beacon received"));
|
|
46
|
-
}
|
|
47
|
-
} else {
|
|
48
|
-
request.log.info(logPayload, logMessage("Beacon received"));
|
|
49
|
-
}
|
|
50
|
-
return true;
|
|
17
|
+
const handleBeaconRequest = async (params) => {
|
|
18
|
+
const { body, options, reply, request } = params;
|
|
19
|
+
let beacon;
|
|
20
|
+
try {
|
|
21
|
+
beacon = parseBeacon(body);
|
|
22
|
+
} catch {
|
|
23
|
+
if (options.onUnknownBeacon) {
|
|
24
|
+
if (!(await options.onUnknownBeacon(body, request, reply))?.skipDefaultLog) request.log.warn({ bodyType: typeof body }, logMessage("Unknown beacon format"));
|
|
25
|
+
} else request.log.warn({ bodyType: typeof body }, logMessage("Unknown beacon format"));
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const logPayload = {
|
|
29
|
+
...beacon.appName && { appName: beacon.appName },
|
|
30
|
+
errorMessage: beacon.errorMessage,
|
|
31
|
+
eventMessage: beacon.eventMessage,
|
|
32
|
+
eventName: beacon.eventName,
|
|
33
|
+
serialized: beacon.serialized
|
|
34
|
+
};
|
|
35
|
+
if (options.onBeacon) {
|
|
36
|
+
if (!(await options.onBeacon(beacon, request, reply))?.skipDefaultLog) request.log.info(logPayload, logMessage("Beacon received"));
|
|
37
|
+
} else request.log.info(logPayload, logMessage("Beacon received"));
|
|
38
|
+
return true;
|
|
51
39
|
};
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
40
|
+
/**
|
|
41
|
+
* SPA Guard plugin for Fastify
|
|
42
|
+
* Registers a POST endpoint to receive beacon data from the client
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { fastifySPAGuard } from '@ovineko/spa-guard/fastify';
|
|
47
|
+
*
|
|
48
|
+
* app.register(fastifySPAGuard, {
|
|
49
|
+
* path: '/api/beacon',
|
|
50
|
+
* onBeacon: async (beacon, request, reply) => {
|
|
51
|
+
* // Handle beacon data (e.g., log to Sentry)
|
|
52
|
+
* const error = new Error(beacon.errorMessage || 'Unknown error');
|
|
53
|
+
* Sentry.captureException(error, {
|
|
54
|
+
* extra: {
|
|
55
|
+
* eventName: beacon.eventName,
|
|
56
|
+
* eventMessage: beacon.eventMessage,
|
|
57
|
+
* serialized: beacon.serialized,
|
|
58
|
+
* },
|
|
59
|
+
* });
|
|
60
|
+
*
|
|
61
|
+
* // Skip default logging if you want to handle it yourself
|
|
62
|
+
* return { skipDefaultLog: true };
|
|
63
|
+
* },
|
|
64
|
+
* });
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
const fastifySPAGuardPlugin = async (fastify, options) => {
|
|
68
|
+
const { onBeacon, onUnknownBeacon, path } = options;
|
|
69
|
+
fastify.post(path, async (request, reply) => {
|
|
70
|
+
if (typeof request.body !== "string") {
|
|
71
|
+
if (onUnknownBeacon) {
|
|
72
|
+
if (!(await onUnknownBeacon(request.body, request, reply))?.skipDefaultLog) request.log.warn({ bodyType: typeof request.body }, logMessage("Invalid beacon body type, expected string"));
|
|
73
|
+
} else request.log.warn({ bodyType: typeof request.body }, logMessage("Invalid beacon body type, expected string"));
|
|
74
|
+
if (!reply.sent) return reply.status(400).send({ error: "Invalid body type" });
|
|
75
|
+
return reply;
|
|
76
|
+
}
|
|
77
|
+
const success = await handleBeaconRequest({
|
|
78
|
+
body: parseStringBody(request.body),
|
|
79
|
+
options: {
|
|
80
|
+
...onBeacon !== void 0 && { onBeacon },
|
|
81
|
+
...onUnknownBeacon !== void 0 && { onUnknownBeacon }
|
|
82
|
+
},
|
|
83
|
+
reply,
|
|
84
|
+
request
|
|
85
|
+
});
|
|
86
|
+
if (!reply.sent) return reply.status(success ? 200 : 400).send(success ? { success: true } : { error: "Invalid beacon format" });
|
|
87
|
+
return reply;
|
|
88
|
+
});
|
|
87
89
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
const fastifySPAGuard = fp(fastifySPAGuardPlugin, {
|
|
91
|
+
fastify: "5.x || 4.x",
|
|
92
|
+
name: `${name}/fastify`
|
|
91
93
|
});
|
|
92
94
|
async function spaGuardFastifyHandler(request, reply, options) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
return reply.status(response.statusCode).send(response.body);
|
|
95
|
+
const { getHtml } = options;
|
|
96
|
+
if (!options.cache && !getHtml) throw new Error("spaGuardFastifyHandler requires either 'cache' or 'getHtml' option");
|
|
97
|
+
if (!options.cache && getHtml) {
|
|
98
|
+
options._cachePromise ??= (async () => createHtmlCache(await getHtml()))().catch((error) => {
|
|
99
|
+
delete options._cachePromise;
|
|
100
|
+
throw error;
|
|
101
|
+
});
|
|
102
|
+
options.cache = await options._cachePromise;
|
|
103
|
+
}
|
|
104
|
+
const cache = options.cache;
|
|
105
|
+
const acceptEncoding = request.headers["accept-encoding"];
|
|
106
|
+
const acceptLanguage = request.headers["accept-language"];
|
|
107
|
+
const ifNoneMatch = request.headers["if-none-match"];
|
|
108
|
+
const response = cache.get({
|
|
109
|
+
...acceptEncoding !== void 0 && { acceptEncoding },
|
|
110
|
+
...acceptLanguage !== void 0 && { acceptLanguage },
|
|
111
|
+
...ifNoneMatch !== void 0 && { ifNoneMatch }
|
|
112
|
+
});
|
|
113
|
+
for (const [key, value] of Object.entries(response.headers)) reply.header(key, value);
|
|
114
|
+
return reply.status(response.statusCode).send(response.body);
|
|
115
115
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
fastifySPAGuard,
|
|
119
|
-
spaGuardFastifyHandler
|
|
120
|
-
};
|
|
116
|
+
//#endregion
|
|
117
|
+
export { BeaconError, fastifySPAGuard, spaGuardFastifyHandler };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ovineko/spa-guard-fastify",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "Fastify plugin for spa-guard beacon endpoint and HTML cache handler with ETag/304",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"spa",
|
|
@@ -36,11 +36,8 @@
|
|
|
36
36
|
"peerDependencies": {
|
|
37
37
|
"fastify": "^5 || ^4",
|
|
38
38
|
"fastify-plugin": "^5 || ^4",
|
|
39
|
-
"@ovineko/spa-guard": "0.0.
|
|
40
|
-
"@ovineko/spa-guard-node": "0.0.
|
|
41
|
-
},
|
|
42
|
-
"engines": {
|
|
43
|
-
"node": ">=22.15.0"
|
|
39
|
+
"@ovineko/spa-guard": "0.0.4",
|
|
40
|
+
"@ovineko/spa-guard-node": "0.0.4"
|
|
44
41
|
},
|
|
45
42
|
"publishConfig": {
|
|
46
43
|
"access": "public"
|