@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 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
- ```sh
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
- ### Beacon endpoint
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
- ### HTML cache handler
46
-
47
- Serve your SPA's `index.html` with ETag/304 and compression negotiation:
16
+ **npm**:
48
17
 
49
- ```ts
50
- import { createReadStream } from "node:fs";
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
- ## API
65
-
66
- ### `fastifySPAGuard` (Fastify plugin)
67
-
68
- Registers a `POST` route at `options.path` to receive beacon payloads.
22
+ **yarn**:
69
23
 
70
- Options:
24
+ ```bash
25
+ yarn add @ovineko/spa-guard-fastify @ovineko/spa-guard @ovineko/spa-guard-node fastify fastify-plugin
26
+ ```
71
27
 
72
- - `path` (required) - Route path for the beacon endpoint, e.g. `"/api/beacon"`
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
- ### `spaGuardFastifyHandler(request, reply, options)`
30
+ ```bash
31
+ bun add @ovineko/spa-guard-fastify @ovineko/spa-guard @ovineko/spa-guard-node fastify fastify-plugin
32
+ ```
77
33
 
78
- Fastify request handler that serves HTML with ETag/304 and content-encoding negotiation.
34
+ **deno**:
79
35
 
80
- Options:
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
- - `cache` - Pre-built `HtmlCache` instance
83
- - `getHtml()` - Async factory returning `CreateHtmlCacheOptions`; cache is created lazily on first request
40
+ ## Usage
84
41
 
85
- ### Exported types
42
+ ```ts
43
+ import Fastify from "fastify";
44
+ import { fastifySPAGuard } from "@ovineko/spa-guard-fastify";
86
45
 
87
- - `FastifySPAGuardOptions`
88
- - `SpaGuardHandlerOptions`
89
- - `BeaconHandlerResult`
90
- - `BeaconError`
46
+ const app = Fastify();
47
+ app.register(fastifySPAGuard, { path: "/api/beacon" });
48
+ ```
91
49
 
92
- ## Related packages
50
+ ## Documentation
93
51
 
94
- - [@ovineko/spa-guard](../spa-guard/README.md) - Core package
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 type { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
2
- import type { CreateHtmlCacheOptions, HtmlCache } from "@ovineko/spa-guard-node";
3
- import type { BeaconSchema } from "@ovineko/spa-guard/schema";
4
- export { BeaconError } from "@ovineko/spa-guard";
5
- export interface BeaconHandlerResult {
6
- /**
7
- * If true, skips default logging behavior
8
- */
9
- skipDefaultLog?: boolean;
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
- export interface FastifySPAGuardOptions {
12
- /**
13
- * Custom handler for beacon data
14
- * @param beacon - Parsed beacon data
15
- * @param request - Fastify request object
16
- * @param reply - Fastify reply object
17
- * @returns Object with options to control default behavior
18
- */
19
- onBeacon?: (beacon: BeaconSchema, request: FastifyRequest, reply: FastifyReply) => BeaconHandlerResult | Promise<BeaconHandlerResult | void> | void;
20
- /**
21
- * Custom handler for invalid/unknown beacon data
22
- * @param body - Raw body data
23
- * @param request - Fastify request object
24
- * @param reply - Fastify reply object
25
- * @returns Object with options to control default behavior
26
- */
27
- onUnknownBeacon?: (body: unknown, request: FastifyRequest, reply: FastifyReply) => BeaconHandlerResult | Promise<BeaconHandlerResult | void> | void;
28
- /**
29
- * The route path for the beacon endpoint
30
- * @example "/api/beacon"
31
- */
32
- path: string;
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
- export declare const fastifySPAGuard: FastifyPluginAsync<FastifySPAGuardOptions>;
35
- export interface SpaGuardHandlerOptions {
36
- /** @internal Promise used to deduplicate concurrent lazy cache creation */
37
- _cachePromise?: Promise<HtmlCache>;
38
- cache?: HtmlCache;
39
- getHtml?: (() => CreateHtmlCacheOptions) | (() => Promise<CreateHtmlCacheOptions>);
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
- export declare function spaGuardFastifyHandler(request: FastifyRequest, reply: FastifyReply, options: SpaGuardHandlerOptions): Promise<FastifyReply>;
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
- // src/index.ts
12
- var parseStringBody = (body) => {
13
- try {
14
- return JSON.parse(body);
15
- } catch {
16
- return body;
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
- var handleBeaconRequest = async (params) => {
20
- const { body, options, reply, request } = params;
21
- let beacon;
22
- try {
23
- beacon = parseBeacon(body);
24
- } catch {
25
- if (options.onUnknownBeacon) {
26
- const result = await options.onUnknownBeacon(body, request, reply);
27
- if (!result?.skipDefaultLog) {
28
- request.log.warn({ bodyType: typeof body }, logMessage("Unknown beacon format"));
29
- }
30
- } else {
31
- request.log.warn({ bodyType: typeof body }, logMessage("Unknown beacon format"));
32
- }
33
- return false;
34
- }
35
- const logPayload = {
36
- ...beacon.appName && { appName: beacon.appName },
37
- errorMessage: beacon.errorMessage,
38
- eventMessage: beacon.eventMessage,
39
- eventName: beacon.eventName,
40
- serialized: beacon.serialized
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
- var fastifySPAGuardPlugin = async (fastify, options) => {
53
- const { onBeacon, onUnknownBeacon, path } = options;
54
- fastify.post(path, async (request, reply) => {
55
- if (typeof request.body !== "string") {
56
- if (onUnknownBeacon) {
57
- const result = await onUnknownBeacon(request.body, request, reply);
58
- if (!result?.skipDefaultLog) {
59
- request.log.warn(
60
- { bodyType: typeof request.body },
61
- logMessage("Invalid beacon body type, expected string")
62
- );
63
- }
64
- } else {
65
- request.log.warn(
66
- { bodyType: typeof request.body },
67
- logMessage("Invalid beacon body type, expected string")
68
- );
69
- }
70
- if (!reply.sent) {
71
- return reply.status(400).send({ error: "Invalid body type" });
72
- }
73
- return reply;
74
- }
75
- const body = parseStringBody(request.body);
76
- const success = await handleBeaconRequest({
77
- body,
78
- options: { onBeacon, onUnknownBeacon },
79
- reply,
80
- request
81
- });
82
- if (!reply.sent) {
83
- return reply.status(success ? 200 : 400).send(success ? { success: true } : { error: "Invalid beacon format" });
84
- }
85
- return reply;
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
- var fastifySPAGuard = fp(fastifySPAGuardPlugin, {
89
- fastify: "5.x || 4.x",
90
- name: `${name}/fastify`
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
- const { getHtml } = options;
94
- if (!options.cache && !getHtml) {
95
- throw new Error("spaGuardFastifyHandler requires either 'cache' or 'getHtml' option");
96
- }
97
- if (!options.cache && getHtml) {
98
- options._cachePromise ??= (async () => createHtmlCache(await getHtml()))().catch(
99
- (error) => {
100
- options._cachePromise = void 0;
101
- throw error;
102
- }
103
- );
104
- options.cache = await options._cachePromise;
105
- }
106
- const cache = options.cache;
107
- const acceptEncoding = request.headers["accept-encoding"];
108
- const acceptLanguage = request.headers["accept-language"];
109
- const ifNoneMatch = request.headers["if-none-match"];
110
- const response = cache.get({ acceptEncoding, acceptLanguage, ifNoneMatch });
111
- for (const [key, value] of Object.entries(response.headers)) {
112
- reply.header(key, value);
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
- export {
117
- BeaconError,
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-alpha-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.3-alpha-0",
40
- "@ovineko/spa-guard-node": "0.0.3-alpha-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"