@stopnetic/js 0.0.4-pilot → 0.0.6-pilot
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 +67 -22
- package/dist/client-guide.md +535 -45
- package/dist/setup.js +3 -1
- package/examples/frontend/angular/app/app.ts +1 -3
- package/examples/frontend/react/App.tsx +1 -1
- package/examples/frontend/solid/App.tsx +2 -2
- package/examples/frontend/svelte/App.svelte +1 -3
- package/examples/frontend/vanilla/main.js +1 -1
- package/examples/frontend/vue/App.vue +1 -2
- package/examples/ssr/angular/app.routes.server.ts +5 -0
- package/examples/ssr/angular/server.ts +12 -0
- package/examples/ssr/astro/middleware.js +11 -0
- package/examples/ssr/astro/page.astro +4 -0
- package/examples/ssr/node/fetch.mjs +9 -0
- package/examples/ssr/node/install.mjs +9 -0
- package/examples/ssr/nuxt/middleware.mjs +10 -0
- package/examples/ssr/react/install.mjs +10 -0
- package/examples/ssr/react-router/install.mjs +22 -0
- package/examples/ssr/solidstart/middleware-v1.js +19 -0
- package/examples/ssr/solidstart/middleware-v2.js +25 -0
- package/examples/ssr/sveltekit/hooks.server.js +16 -0
- package/examples/ssr/sveltekit/page.server.js +3 -0
- package/examples/ssr/vue/install.mjs +10 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -6,18 +6,23 @@ frontend build. No React/Vue/Angular dependency or version upgrade is required.
|
|
|
6
6
|
The hosting server must run Node.js 22 or newer. This is an MIT-licensed pilot SDK;
|
|
7
7
|
hosted production/provider validation is still required before paid rollout.
|
|
8
8
|
|
|
9
|
-
Version **0.0.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Version **0.0.6-pilot is prepared locally; it is not published yet**. It adds
|
|
10
|
+
framework-specific SSR instructions and a dashboard SSR selector for Angular,
|
|
11
|
+
React Router, custom React SSR, Nuxt, custom Vue SSR, SvelteKit, Astro, SolidStart
|
|
12
|
+
and custom Node servers. Examples identify the actual server entry, prerender
|
|
13
|
+
settings, deployed Node runtime and context/body preservation. Next.js uses its
|
|
14
|
+
dedicated adapter; other languages/Edge are explicitly outside this Node SDK.
|
|
15
|
+
Unchanged 0.0.2/0.0.3/0.0.4/0.0.5 guides migrate safely; custom edits require merging.
|
|
16
|
+
SSR and static rendering remain separate choices, with document navigation
|
|
17
|
+
default and no global checking screen. Optional browser demos remain available.
|
|
13
18
|
Angular, React, Vue, Svelte, Solid and plain JavaScript share the browser/server
|
|
14
19
|
contracts; no React dependency is required. See the
|
|
15
20
|
[complete generated guide](dist/client-guide.md) for SSR, static builds, frameworks,
|
|
16
21
|
client navigation, API protection and runtime restrictions.
|
|
17
22
|
|
|
18
|
-
The currently published **0.0.
|
|
19
|
-
server/browser setup, complete Node application wrapper
|
|
20
|
-
migration. Version 0.0.2 added the reusable browser/server setup for Netlify, Vercel
|
|
23
|
+
The currently published **0.0.5-pilot** clarifies rendering/navigation and includes full frontend examples and the
|
|
24
|
+
framework-independent server/browser setup, complete Node application wrapper
|
|
25
|
+
and older-guide migration. Version 0.0.2 added the reusable browser/server setup for Netlify, Vercel
|
|
21
26
|
and Node/Express, a browser access check and optional React boundary. It also
|
|
22
27
|
normalizes public PEM formatting and rejects placeholder or reused installation
|
|
23
28
|
keys as cookie secrets. Install the published pilot with:
|
|
@@ -28,7 +33,44 @@ npm install @stopnetic/js@pilot
|
|
|
28
33
|
|
|
29
34
|
## Framework implementation examples
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
SSR apps use the **SSR framework** selector, not the optional browser-demo
|
|
37
|
+
selector. [Framework SSR instructions](dist/client-guide.md#ssr-framework-instructions)
|
|
38
|
+
and `examples/ssr` contain matching server code:
|
|
39
|
+
|
|
40
|
+
| SSR app | Server connection |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| Angular | AngularAppEngine Fetch entry or Express before AngularNodeAppEngine |
|
|
43
|
+
| React Router | Existing Express entry; single-fetch `.data` aliases use page policy |
|
|
44
|
+
| Custom React / Vue | Guard before the existing Node renderer; keep hydration |
|
|
45
|
+
| Nuxt 3/4 (Nitro 2/H3 1) | Root Node server middleware before rendering/APIs |
|
|
46
|
+
| SvelteKit | `handle` before `resolve(event)`; canonical page check for data requests |
|
|
47
|
+
| Astro | `onRequest` before `next()` with an existing Node adapter |
|
|
48
|
+
| SolidStart 1 / 2 | Version-specific request middleware before continuation |
|
|
49
|
+
| Custom Node / plain JavaScript | Root Express middleware or complete Fetch wrapper |
|
|
50
|
+
| Next.js | Dedicated `@stopnetic/next` guide |
|
|
51
|
+
|
|
52
|
+
Merge existing files/context, preserve public routes and keep protected output
|
|
53
|
+
out of prerender/static/CDN caches. These manual snippets are not automatic
|
|
54
|
+
framework installers. Node compatibility does not validate a live provider.
|
|
55
|
+
Only dashboard rules decide verification; Angular-specific flags are not instructions
|
|
56
|
+
for other frameworks. React/Vue libraries without a server use the static option.
|
|
57
|
+
|
|
58
|
+
To test server continuation contracts after `npm run build`, run
|
|
59
|
+
`node tests/prepare-ssr-guide-tests.mjs`, then `node tests/ssr-guide-check.mjs`.
|
|
60
|
+
Dependencies remain in ignored test-results. Native H3 1/2 and Express exercise
|
|
61
|
+
signed access, control isolation, data aliases and unread POST bodies. Astro,
|
|
62
|
+
SvelteKit and SolidStart factory imports use contract shims, not full framework
|
|
63
|
+
builds. Angular's separate fixture compiles/renders real SSR. Live routing,
|
|
64
|
+
prerender/cache behavior and complete framework/provider builds still need validation.
|
|
65
|
+
|
|
66
|
+
The guide and `examples/frontend` contain complete, locally tested OPTIONAL
|
|
67
|
+
browser-only SPA examples. The default document-navigation integration needs
|
|
68
|
+
the server guard and normal links, not these full-layout browser gates. Keep SSR
|
|
69
|
+
and hydration; do not apply the browser demos to an existing SSR root. A runtime
|
|
70
|
+
incompatibility needs a deployment decision, not an automatic conversion to CSR.
|
|
71
|
+
Dashboard policies decide which paths require verification; demo paths are not
|
|
72
|
+
instructions to protect every page. SPA checks still evaluate public targets
|
|
73
|
+
asynchronously and can show a brief empty pending state. The examples are:
|
|
32
74
|
|
|
33
75
|
| Frontend | Example |
|
|
34
76
|
| --- | --- |
|
|
@@ -40,7 +82,7 @@ The guide and `examples/frontend` contain complete, locally tested examples:
|
|
|
40
82
|
| Plain JavaScript | DOM rendering, checked navigation and API loading |
|
|
41
83
|
|
|
42
84
|
Each example includes initial access, later navigation, verification redirect,
|
|
43
|
-
pending
|
|
85
|
+
empty pending content, denied UI and cancellation. Backend page/API guards remain necessary.
|
|
44
86
|
No framework library is added as a required SDK dependency. The generated guide
|
|
45
87
|
embeds the source; use its file paths and helper instructions when copying it.
|
|
46
88
|
The dashboard's frontend selector shows the same files. For maintenance, run
|
|
@@ -62,7 +104,7 @@ npm run build
|
|
|
62
104
|
npm pack
|
|
63
105
|
```
|
|
64
106
|
|
|
65
|
-
Install the resulting `stopnetic-js-0.0.
|
|
107
|
+
Install the resulting `stopnetic-js-0.0.6-pilot.tgz` into the website project using
|
|
66
108
|
`npm install <absolute-path-to-tarball>`. The installed package has no Next.js or
|
|
67
109
|
other runtime dependency. The build packages the same signed decision implementation
|
|
68
110
|
as the Next.js adapter. Keep `@stopnetic/next` for its Next.js 16 guided installer.
|
|
@@ -98,7 +140,8 @@ preserved. Vercel configuration is preserved. Put runtime settings in the hostin
|
|
|
98
140
|
provider's server/Functions environment; plain `netlify.toml` build env is not a
|
|
99
141
|
Functions secret store. Copy the generated cookie secret without committing it.
|
|
100
142
|
|
|
101
|
-
Wire
|
|
143
|
+
Wire the server page guard and EACH protected API following the generated guide.
|
|
144
|
+
Browser route checks are optional for retained SPA navigation.
|
|
102
145
|
Run `npm exec --no -- stopnetic-js check` after setting credentials (`--local-http`
|
|
103
146
|
for a loopback pilot). This check validates configuration and generated files
|
|
104
147
|
offline; it cannot prove deployment routing, API coverage or browser integration.
|
|
@@ -111,12 +154,11 @@ server environment loader. Production credentials belong in runtime settings.
|
|
|
111
154
|
|
|
112
155
|
### Shared browser startup and Node SSR
|
|
113
156
|
|
|
114
|
-
The generated [framework-independent guide](
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
or recheck target access in the client router. A browser-only check is UI behavior.
|
|
157
|
+
The generated [framework-independent guide](dist/client-guide.md) defaults to
|
|
158
|
+
server document navigation: leave browser startup/layout intact, use normal
|
|
159
|
+
links, and let the server apply dashboard policies. It also gives an OPTIONAL
|
|
160
|
+
browser startup gate and separate OPTIONAL SPA router demos. Do not combine
|
|
161
|
+
the alternatives or apply browser-only gates to an SSR hydration root.
|
|
120
162
|
|
|
121
163
|
For Node SSR use the generated Express helper before static files/rendering,
|
|
122
164
|
or `createStopneticAppHandler` around a Fetch-style application entry (0.0.3).
|
|
@@ -128,15 +170,17 @@ adapted; accepting Request/Response does not make an Edge runtime supported.
|
|
|
128
170
|
The guide includes Angular SSR/rendering and client router examples. Angular
|
|
129
171
|
SSR compiled with the installed Angular 22 dependencies and served real HTML
|
|
130
172
|
after verification in an isolated local fixture. That does not validate a live
|
|
131
|
-
provider deployment. Netlify's automatic Angular SSR uses Edge;
|
|
132
|
-
|
|
173
|
+
provider deployment. Netlify's automatic Angular SSR uses Edge; the Node SDK
|
|
174
|
+
has no drop-in integration for that runtime. Preserve SSR and obtain a supported
|
|
175
|
+
Node deployment/gateway decision instead of silently converting the app. Do not publish protected prerendered
|
|
133
176
|
HTML. For other frameworks use the same Node server entry and verify their
|
|
134
177
|
actual deployed runtime. React's boundary remains an optional convenience.
|
|
135
178
|
|
|
136
179
|
### Optional React Router boundary
|
|
137
180
|
|
|
138
181
|
The optional React entry uses Hooks. The core/browser modules need no React.
|
|
139
|
-
|
|
182
|
+
Use this only for explicitly retained browser SPA navigation, inside BrowserRouter,
|
|
183
|
+
around the full target layout. Server document navigation does not require it:
|
|
140
184
|
|
|
141
185
|
```jsx
|
|
142
186
|
import { useLocation } from 'react-router-dom';
|
|
@@ -144,11 +188,12 @@ import { StopneticBoundary } from '@stopnetic/js/react';
|
|
|
144
188
|
|
|
145
189
|
function RouteGuard({ children }) {
|
|
146
190
|
const location = useLocation();
|
|
147
|
-
return <StopneticBoundary path={location.pathname + location.search}>
|
|
191
|
+
return <StopneticBoundary path={location.pathname + location.search} pending={null}>
|
|
148
192
|
{children}
|
|
149
193
|
</StopneticBoundary>;
|
|
150
194
|
}
|
|
151
|
-
// <BrowserRouter><RouteGuard><
|
|
195
|
+
// <BrowserRouter><RouteGuard><Layout/></RouteGuard></BrowserRouter>
|
|
196
|
+
// Layout includes the target header, routes and footer.
|
|
152
197
|
```
|
|
153
198
|
|
|
154
199
|
The boundary hides children while checking, ignores canceled/stale checks and
|
package/dist/client-guide.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Angular, React, Vue, Svelte, Solid and plain JavaScript use the same server guard
|
|
4
4
|
and browser access check. React is optional. Install `@stopnetic/js` in the
|
|
5
|
-
website project
|
|
5
|
+
website project. The CLI chooses hosting files; choose your actual SSR framework
|
|
6
|
+
in the instructions below to see where its server guard belongs.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Start with [SSR framework instructions](#ssr-framework-instructions) if the app
|
|
9
|
+
renders on the server. Optional browser-only demos: [React Router](#react-router),
|
|
8
10
|
[Angular Router](#angular-router), [Vue Router](#vue-router),
|
|
9
11
|
[Svelte](#svelte-navigation), [Solid Router](#solid-router),
|
|
10
12
|
[plain JavaScript](#plain-javascript-navigation). Each section includes the
|
|
@@ -13,8 +15,61 @@ layout, navigation and protected API example, not just the startup function.
|
|
|
13
15
|
The generated control endpoint is only part of installation. It does not
|
|
14
16
|
automatically intercept pages, edit your router or protect existing APIs.
|
|
15
17
|
|
|
18
|
+
## Integration contract — read before editing an existing app
|
|
19
|
+
|
|
20
|
+
1. Inspect the existing server entry, rendering modes, hosting adapter and router.
|
|
21
|
+
Preserve SSR, hydration, public pages, routes and application features.
|
|
22
|
+
Installing Stopnetic is not authorization to convert SSR to CSR, delete server
|
|
23
|
+
files, replace the application's router or copy the demo over an existing app.
|
|
24
|
+
2. Choose ONE of the two rendering paths below. Keep the app's current rendering
|
|
25
|
+
by default. A hosting/runtime incompatibility must be explained to the owner;
|
|
26
|
+
do not resolve it by silently removing SSR or changing hosting.
|
|
27
|
+
3. Install the guard before the existing server renders or serves HTML. Keep the
|
|
28
|
+
existing browser bootstrap unchanged for the default document-navigation mode.
|
|
29
|
+
Do not add a global `Checking access...` screen or a browser startup gate.
|
|
30
|
+
4. Save protected paths in Stopnetic Policies. Do not add the demo paths, protect
|
|
31
|
+
every page, copy dashboard rules into a frontend path list, or change defaults
|
|
32
|
+
without the owner's instruction. Guards evaluate requests; they do not make
|
|
33
|
+
every evaluated request require verification.
|
|
34
|
+
5. Use normal document navigation for the simplest server-enforced integration.
|
|
35
|
+
If retaining client-side SPA navigation is required, use the OPTIONAL router
|
|
36
|
+
examples and explain their asynchronous checks before adding them.
|
|
37
|
+
6. Independently guard private API handlers. Validate public, protected and denied
|
|
38
|
+
pages plus SSR output before declaring integration complete.
|
|
39
|
+
|
|
40
|
+
## What visitors should see
|
|
41
|
+
|
|
42
|
+
For example, suppose only `/members` is Page / Automatic browser flow:
|
|
43
|
+
|
|
44
|
+
| Request | Normal behavior |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `/`, `/about`, `/contact` with no matching rule | Public page; normal layout, no Stopnetic checking screen or verification |
|
|
47
|
+
| `/members` without a valid session | Server verification flow before returning the page |
|
|
48
|
+
| `/members` with a valid session | Normal page content |
|
|
49
|
+
| A Page / Block route | Empty HTTP 404 before any site layout renders |
|
|
50
|
+
| A protected API without a valid session | HTTP 404; no JSON data and no browser challenge |
|
|
51
|
+
|
|
52
|
+
Generated site mode leaves unmatched paths public; explicit broader rules can
|
|
53
|
+
change that. Public requests still need a valid signed policy response. A service
|
|
54
|
+
failure returns 503 rather than bypassing policy. Rules belong in the dashboard;
|
|
55
|
+
the browser cannot know whether a target is public until the server checks it.
|
|
56
|
+
|
|
16
57
|
## 1. Choose the actual rendering and server runtime
|
|
17
58
|
|
|
59
|
+
**Option A — preserve SSR.** Keep the existing renderer, server entry and
|
|
60
|
+
hydration. Add the Node guard before the renderer (section 3). Use server
|
|
61
|
+
document navigation (section 4). Public pages render normally; protected pages
|
|
62
|
+
render only after the dashboard decision permits them.
|
|
63
|
+
|
|
64
|
+
**Option B — retain a static/CSR frontend.** Keep the existing browser bootstrap
|
|
65
|
+
and build. Add a Node hosting page guard before HTML (section 3), then use server
|
|
66
|
+
document navigation (section 4). A Node function serving built `index.html` is
|
|
67
|
+
static page handling, not SSR. Choose this option for an existing static app or
|
|
68
|
+
only after an explicit decision to convert rendering.
|
|
69
|
+
|
|
70
|
+
Neither option requires a global browser boundary. Optional SPA navigation is
|
|
71
|
+
a separate choice, not a third rendering mode.
|
|
72
|
+
|
|
18
73
|
| Application | Where the shared guard belongs |
|
|
19
74
|
| --- | --- |
|
|
20
75
|
| Node/Express SSR or static-file server | Before body parsers, static files, API handlers and rendering |
|
|
@@ -95,11 +150,57 @@ the shared guard before its `resolve`, `next` or renderer. A Fetch wrapper is
|
|
|
95
150
|
for a complete handler returning a Response, not every framework middleware
|
|
96
151
|
signature. Node/Express can mount the generated helper around any renderer.
|
|
97
152
|
|
|
153
|
+
<!-- BEGIN SSR EXAMPLES -->
|
|
154
|
+
|
|
155
|
+
### SSR framework instructions
|
|
156
|
+
|
|
157
|
+
Choose the actual SSR framework, not just the browser UI library. Angular
|
|
158
|
+
instructions apply ONLY to Angular. Each section below identifies the server
|
|
159
|
+
file, integration steps, prerender constraints and runtime. The same sections
|
|
160
|
+
appear in the dashboard SSR framework selector. Merge examples into existing
|
|
161
|
+
entries; do not replace SSR, hydration, context or application features.
|
|
162
|
+
|
|
163
|
+
Use your selected framework hook OR the shared Node wrapper above. Do not
|
|
164
|
+
stack both or mount the generated Express helper as an additional guard.
|
|
165
|
+
|
|
166
|
+
| Your app | Choose |
|
|
167
|
+
| --- | --- |
|
|
168
|
+
| Angular with SSR | Angular SSR |
|
|
169
|
+
| React Router framework/server mode | React Router SSR |
|
|
170
|
+
| React plus your own Node renderer | React custom SSR |
|
|
171
|
+
| Nuxt | Nuxt (Vue SSR) |
|
|
172
|
+
| Vue plus your own Node renderer | Vue custom SSR |
|
|
173
|
+
| SvelteKit | SvelteKit SSR |
|
|
174
|
+
| Astro on-demand rendering | Astro SSR |
|
|
175
|
+
| SolidStart | SolidStart SSR; match the installed major version |
|
|
176
|
+
| Other Node renderer / plain JavaScript server | Other Node SSR |
|
|
177
|
+
| Next.js | Dedicated @stopnetic/next guide; do not install the JS control files |
|
|
178
|
+
| React/Vue/Svelte/Solid UI without an SSR server | Option B: static/browser app |
|
|
179
|
+
| PHP, Python, Ruby, C#, Java or Edge/Deno | No native adapter in this Node npm SDK; a separate runtime adapter/gateway is needed |
|
|
180
|
+
|
|
181
|
+
All snippets require SDK 0.0.3-pilot or newer and six SERVER settings. They
|
|
182
|
+
are manual integration examples, not automatic installers for each framework.
|
|
183
|
+
Validate the actual deployed adapter, versions, internal data endpoints and
|
|
184
|
+
cache/static aliases. Hook-contract checks do not prove provider deployment.
|
|
185
|
+
|
|
98
186
|
### Angular SSR on Node
|
|
99
187
|
|
|
100
|
-
|
|
188
|
+
**Where:** src/server.ts (Fetch entry), or your existing Express server.ts before AngularNodeAppEngine.handle.
|
|
189
|
+
|
|
190
|
+
1. Keep server.ts, main.server.ts, server providers, host configuration and hydration. Merge the guard into the existing request entry; preserve extra framework context.
|
|
191
|
+
2. Use the Fetch example only for an AngularAppEngine Request/Response entry. An Express entry instead mounts the Node helper before express.static and AngularNodeAppEngine.handle.
|
|
192
|
+
3. Protected paths use RenderMode.Server. The wildcard below is an example, not a replacement for existing public/hybrid route configuration.
|
|
193
|
+
|
|
194
|
+
**Prerendering and caches:** Do not publish protected RenderMode.Prerender output to a public CDN. Retain public rendering modes only where they cannot bypass future protection. Any path the dashboard may protect must reach the server guard before HTML.
|
|
195
|
+
|
|
196
|
+
**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.
|
|
197
|
+
|
|
198
|
+
**Netlify Angular only:** Netlify’s automatic Angular SSR uses Edge Functions. The current Node SDK has no drop-in integration for that combination. Keep SSR; obtain a supported Node deployment or gateway decision. A static index.html function is not an Angular SSR adapter.
|
|
199
|
+
|
|
200
|
+
#### src/server.ts
|
|
101
201
|
|
|
102
202
|
```ts
|
|
203
|
+
// Merge into your existing Node SSR entry; retain host settings and context.
|
|
103
204
|
import { AngularAppEngine, createRequestHandler } from '@angular/ssr';
|
|
104
205
|
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
105
206
|
|
|
@@ -113,26 +214,364 @@ async function handle(request: Request): Promise<Response> {
|
|
|
113
214
|
export const reqHandler = createRequestHandler(handle);
|
|
114
215
|
```
|
|
115
216
|
|
|
116
|
-
|
|
117
|
-
uses them. For an Express-based Angular entry, mount `installStopnetic(app)`
|
|
118
|
-
before `express.static` and `AngularNodeAppEngine.handle` instead.
|
|
119
|
-
|
|
120
|
-
Protected routes in `src/app/app.routes.server.ts` must use request-time
|
|
121
|
-
rendering rather than `RenderMode.Prerender`:
|
|
217
|
+
#### src/app/app.routes.server.ts
|
|
122
218
|
|
|
123
219
|
```ts
|
|
124
220
|
import { RenderMode, ServerRoute } from '@angular/ssr';
|
|
125
221
|
export const serverRoutes: ServerRoute[] = [
|
|
126
|
-
//
|
|
222
|
+
// Demo only: merge with existing hybrid modes rather than replacing them.
|
|
127
223
|
{ path: '**', renderMode: RenderMode.Server },
|
|
128
224
|
];
|
|
129
225
|
```
|
|
130
226
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
227
|
+
Framework references: [Angular SSR](https://angular.dev/guide/ssr), [Netlify Angular runtime](https://docs.netlify.com/build/frameworks/framework-setup-guides/angular/).
|
|
228
|
+
|
|
229
|
+
### React Router framework mode on Node
|
|
230
|
+
|
|
231
|
+
**Where:** Existing Express entry before @react-router/express createRequestHandler.
|
|
232
|
+
|
|
233
|
+
1. React Router framework mode supplies the SSR server. Keep its server build, loaders/actions, getLoadContext and hydration; BrowserRouter alone is not an SSR backend.
|
|
234
|
+
2. Copy the helper, then import and call it at the application root before parsers/assets and the existing createRequestHandler. Do not place it after the renderer or mount it at a stripped URL prefix.
|
|
235
|
+
3. The helper checks React Router 7 .data requests against their page path, including /_root.data. The actual router still receives its original URL and unread body. This example uses the default / basename; custom basenames and other router versions need alias mapping review.
|
|
236
|
+
|
|
237
|
+
**Prerendering and caches:** Keep ssr enabled in react-router.config.ts. Exclude protected paths from prerender output and CDN caches. Direct .data requests must not expose private loaders merely because the document path is guarded. Independently authorize resource routes, actions and server functions.
|
|
238
|
+
|
|
239
|
+
**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.
|
|
240
|
+
|
|
241
|
+
#### stopnetic-react-router.mjs
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
// Call installStopneticReactRouter(app) before parsers, assets and the existing
|
|
245
|
+
// @react-router/express createRequestHandler. Keep that handler/context intact.
|
|
246
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
247
|
+
|
|
248
|
+
export function installStopneticReactRouter(app) {
|
|
249
|
+
let server;
|
|
250
|
+
app.use((req, res, next) => {
|
|
251
|
+
server ??= createStopneticServer();
|
|
252
|
+
return server.access(req, res, () => {
|
|
253
|
+
// React Router 7 single-fetch data URLs must use the page's policy.
|
|
254
|
+
// The renderer still receives the original request and unread body.
|
|
255
|
+
const url = new URL(req.originalUrl || req.url, 'https://example.invalid');
|
|
256
|
+
let checked = req;
|
|
257
|
+
if (url.pathname.endsWith('.data')) {
|
|
258
|
+
url.pathname = url.pathname === '/_root.data' ? '/' : url.pathname.slice(0, -5);
|
|
259
|
+
checked = Object.create(req);
|
|
260
|
+
checked.originalUrl = url.pathname + url.search;
|
|
261
|
+
}
|
|
262
|
+
return server.guard(checked, res, next);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
#### Existing Express entry (merge fragment)
|
|
269
|
+
|
|
270
|
+
```js
|
|
271
|
+
// In your EXISTING Express entry, before parsers, static files and rendering:
|
|
272
|
+
import { installStopneticReactRouter } from './stopnetic-react-router.mjs';
|
|
273
|
+
installStopneticReactRouter(app);
|
|
274
|
+
// Keep the existing renderer, application routes and server startup below.
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Framework references: [React Router server adapters](https://reactrouter.com/api/other-api/adapter), [React Router prerendering](https://reactrouter.com/how-to/pre-rendering).
|
|
278
|
+
|
|
279
|
+
### React with an existing custom SSR server
|
|
280
|
+
|
|
281
|
+
**Where:** Existing Node/Express server before React rendering and any private data handler.
|
|
282
|
+
|
|
283
|
+
1. React is a UI library; this option assumes you already have a Node SSR server. Keep renderToPipeableStream/renderToReadableStream, request context and hydrateRoot.
|
|
284
|
+
2. Copy the helper and call it before the existing assets/API/renderer middleware. For a Fetch-style server use the custom Node Fetch wrapper instead.
|
|
285
|
+
3. A Vite React SPA without a server uses Option B: static/browser app. Next.js and React Router SSR have their own selector entries. Do not add a second renderer or replace the app with the browser demo.
|
|
286
|
+
|
|
287
|
+
**Prerendering and caches:** Do not statically publish protected HTML or embed private data in public bundles. Keep guarded rendering at request time, retaining public prerendering only with correct server/CDN coverage.
|
|
288
|
+
|
|
289
|
+
**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.
|
|
290
|
+
|
|
291
|
+
#### stopnetic-react-ssr.mjs
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
// Existing custom React SSR server: call before assets, APIs and React rendering.
|
|
295
|
+
// Keep renderToPipeableStream/renderToReadableStream and hydration unchanged.
|
|
296
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
297
|
+
export function installStopneticReactSSR(app) {
|
|
298
|
+
let server;
|
|
299
|
+
app.use((req, res, next) => {
|
|
300
|
+
server ??= createStopneticServer();
|
|
301
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### Existing Express entry (merge fragment)
|
|
307
|
+
|
|
308
|
+
```js
|
|
309
|
+
// In your EXISTING Express entry, before parsers, static files and rendering:
|
|
310
|
+
import { installStopneticReactSSR } from './stopnetic-react-ssr.mjs';
|
|
311
|
+
installStopneticReactSSR(app);
|
|
312
|
+
// Keep the existing renderer, application routes and server startup below.
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Framework references: [React Node SSR](https://react.dev/reference/react-dom/server/renderToPipeableStream).
|
|
316
|
+
|
|
317
|
+
### Nuxt 3/4 with Nitro 2 and H3 1 on Node
|
|
318
|
+
|
|
319
|
+
**Where:** server/middleware/00-stopnetic.mjs; runs before Nitro renders pages and serves API handlers.
|
|
320
|
+
|
|
321
|
+
1. Keep Nuxt SSR, its app, pages, hydration and existing Node Nitro preset. Check the installed Nitro/H3 versions; this example uses H3 1 fromNodeMiddleware, not the H3 2 API.
|
|
322
|
+
2. Add this server middleware before other body readers and handlers. app/middleware and defineNuxtRouteMiddleware are navigation UI hooks and cannot replace this server guard.
|
|
323
|
+
3. Keep the private environment settings in the deployed Node process. Do not use runtimeConfig.public. Test the built server and separate provider functions, not only Nuxt dev.
|
|
324
|
+
|
|
325
|
+
**Prerendering and caches:** Do not use nuxt generate or prerender/SWR/ISR/cached route rules for protected paths. Protected _payload.json files must not be generated into public output. Keep public route rules only where they cannot expose protected content through CDN/cache aliases.
|
|
326
|
+
|
|
327
|
+
**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.
|
|
328
|
+
|
|
329
|
+
#### server/middleware/00-stopnetic.mjs
|
|
330
|
+
|
|
331
|
+
```js
|
|
332
|
+
// server/middleware/00-stopnetic.mjs. Nuxt 3/4, Nitro 2, H3 1, NODE only.
|
|
333
|
+
// Use server middleware, not app/middleware or defineNuxtRouteMiddleware.
|
|
334
|
+
import { fromNodeMiddleware } from 'h3';
|
|
335
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
336
|
+
|
|
337
|
+
let server;
|
|
338
|
+
export default fromNodeMiddleware((req, res, next) => {
|
|
339
|
+
server ??= createStopneticServer();
|
|
340
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Framework references: [Nuxt server middleware](https://nuxt.com/docs/4.x/directory-structure/server), [H3 1 Node middleware adapter](https://v1.h3.dev/examples/from-expressjs-to-h3).
|
|
345
|
+
|
|
346
|
+
### Vue with an existing custom SSR server
|
|
347
|
+
|
|
348
|
+
**Where:** Existing Node/Express server before Vue rendering and private APIs.
|
|
349
|
+
|
|
350
|
+
1. Vue alone is a UI library. Keep the existing createSSRApp/renderToString server, request context and client hydration.
|
|
351
|
+
2. Copy and mount the helper before the existing parsers/assets/API/renderer middleware. For a Request/Response server use the custom Node Fetch wrapper.
|
|
352
|
+
3. A Vue SPA uses Option B. A Nuxt app uses the Nuxt entry; do not replace either with a newly invented Express renderer.
|
|
353
|
+
|
|
354
|
+
**Prerendering and caches:** Protected HTML and private serialized state must be produced only after the server check. Avoid publicly cached/prerendered output for protected paths.
|
|
355
|
+
|
|
356
|
+
**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.
|
|
357
|
+
|
|
358
|
+
#### stopnetic-vue-ssr.mjs
|
|
359
|
+
|
|
360
|
+
```js
|
|
361
|
+
// Existing custom Vue SSR server: call before assets, APIs and Vue rendering.
|
|
362
|
+
// Keep createSSRApp/renderToString and client hydration unchanged.
|
|
363
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
364
|
+
export function installStopneticVueSSR(app) {
|
|
365
|
+
let server;
|
|
366
|
+
app.use((req, res, next) => {
|
|
367
|
+
server ??= createStopneticServer();
|
|
368
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
#### Existing Express entry (merge fragment)
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
// In your EXISTING Express entry, before parsers, static files and rendering:
|
|
377
|
+
import { installStopneticVueSSR } from './stopnetic-vue-ssr.mjs';
|
|
378
|
+
installStopneticVueSSR(app);
|
|
379
|
+
// Keep the existing renderer, application routes and server startup below.
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Framework references: [Vue SSR](https://vuejs.org/guide/scaling-up/ssr.html).
|
|
383
|
+
|
|
384
|
+
### SvelteKit server hooks on Node
|
|
385
|
+
|
|
386
|
+
**Where:** src/hooks.server.js, before resolve(event) and any existing application hooks.
|
|
387
|
+
|
|
388
|
+
1. Keep the existing Node adapter, SSR pages, loads/actions and hydration. Merge this handle into existing hooks; put Stopnetic first and retain other hooks in its continuation.
|
|
389
|
+
2. The guard receives the original request and per-request continuation. Data requests are checked against event.url, the canonical page path, then resolve receives the original event and body.
|
|
390
|
+
3. Merge prerender=false into each protected +page.server.js or a covering server layout. building skips runtime enforcement only during build; no protected path may be prerendered. Test with the full adapter server.
|
|
391
|
+
|
|
392
|
+
**Prerendering and caches:** Keep SSR enabled. The runtime handle hook does not protect files already served statically. Exclude protected routes from prerender output, including __data.json. Do not rely on a page rule alone for separately exposed endpoints or remote functions.
|
|
393
|
+
|
|
394
|
+
**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.
|
|
395
|
+
|
|
396
|
+
#### src/hooks.server.js
|
|
397
|
+
|
|
398
|
+
```js
|
|
399
|
+
import { building } from '$app/environment';
|
|
400
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
401
|
+
|
|
402
|
+
let guarded;
|
|
403
|
+
// When merging existing hooks, run this first and put them in the continuation.
|
|
404
|
+
export async function handle({ event, resolve }) {
|
|
405
|
+
// Build-time public prerendering only. Protected routes must have prerender=false.
|
|
406
|
+
if (building) return resolve(event);
|
|
407
|
+
guarded ??= createStopneticAppHandler((_request, run) => run());
|
|
408
|
+
// SvelteKit exposes the canonical page URL on data requests. Check that page
|
|
409
|
+
// rather than allowing an unmatched /members/__data.json alias through.
|
|
410
|
+
const checked = event.isDataRequest
|
|
411
|
+
? new Request(event.url, { method: event.request.method, headers: event.request.headers })
|
|
412
|
+
: event.request;
|
|
413
|
+
return guarded(checked, () => resolve(event));
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### src/routes/members/+page.server.js (merge example)
|
|
418
|
+
|
|
419
|
+
```js
|
|
420
|
+
// Merge into the PROTECTED route's +page.server.js, retaining loads/actions.
|
|
421
|
+
export const prerender = false;
|
|
422
|
+
export const ssr = true;
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Framework references: [SvelteKit server hooks](https://svelte.dev/docs/kit/hooks), [SvelteKit page options](https://svelte.dev/docs/kit/page-options).
|
|
426
|
+
|
|
427
|
+
### Astro request middleware on Node
|
|
428
|
+
|
|
429
|
+
**Where:** src/middleware.js, before next() renders a page or executes an endpoint.
|
|
430
|
+
|
|
431
|
+
1. Keep the existing compatible Node adapter and Astro renderer. If middleware exists, merge/sequence this guard first, preserving locals and the existing continuation.
|
|
432
|
+
2. Use context.request and next(), not a browser startup gate. The isPrerendered branch is build-only public rendering; protected pages must never enter it.
|
|
433
|
+
3. Set prerender=false on protected .astro pages/endpoints. Keep their existing content. For @astrojs/node middleware mode, an outer Express guard before static files is also available; do not install both patterns unnecessarily.
|
|
434
|
+
|
|
435
|
+
**Prerendering and caches:** Protected pages need on-demand rendering with a Node adapter. Public prerendered pages can remain public, but files bypassing request middleware cannot acquire protection through a dashboard rule alone.
|
|
436
|
+
|
|
437
|
+
**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.
|
|
438
|
+
|
|
439
|
+
#### src/middleware.js
|
|
440
|
+
|
|
441
|
+
```js
|
|
442
|
+
// src/middleware.js. Keep the existing Node deployment adapter.
|
|
443
|
+
import { defineMiddleware } from 'astro:middleware';
|
|
444
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
445
|
+
|
|
446
|
+
let guarded;
|
|
447
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
448
|
+
// Public prerender builds only; protected pages must export prerender=false.
|
|
449
|
+
if (context.isPrerendered) return next();
|
|
450
|
+
guarded ??= createStopneticAppHandler((_request, run) => run());
|
|
451
|
+
return guarded(context.request, () => next());
|
|
452
|
+
});
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
#### src/pages/members.astro (frontmatter flag)
|
|
456
|
+
|
|
457
|
+
```astro
|
|
458
|
+
---
|
|
459
|
+
// Merge this flag into a protected page, preserving its existing content.
|
|
460
|
+
export const prerender = false;
|
|
461
|
+
---
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
Framework references: [Astro middleware](https://docs.astro.build/en/guides/middleware/), [Astro Node adapter](https://docs.astro.build/en/guides/integrations-guide/node/).
|
|
465
|
+
|
|
466
|
+
### SolidStart request middleware on Node
|
|
467
|
+
|
|
468
|
+
**Where:** src/middleware.js, registered in the existing SolidStart config before rendering and API execution.
|
|
469
|
+
|
|
470
|
+
1. Choose the snippet matching your installed major version. SolidStart 2 uses createMiddleware([...]) and H3 event.req/event.res; SolidStart 1 uses onRequest and event.request/event.response.
|
|
471
|
+
2. For v2 register solidStart({ middleware: "./src/middleware.js" }) in existing vite.config.ts. For v1 register middleware in existing defineConfig in app.config.ts. Keep all other settings and the SSR entry-server.tsx.
|
|
472
|
+
3. A per-request allowed marker distinguishes an authorized continuation from a verification 204. The framework continues only when the marker is set; no renderer runs on challenge/block/outage. Server functions still require their own permissions and policy checks for their actual endpoint paths.
|
|
473
|
+
|
|
474
|
+
**Prerendering and caches:** Keep SSR enabled and protected routes out of prerender/static/ISR output. Use the existing Node server plugin/preset; do not assume a middleware export changes an Edge deployment into Node.
|
|
475
|
+
|
|
476
|
+
**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.
|
|
477
|
+
|
|
478
|
+
#### src/middleware.js — SolidStart 2
|
|
479
|
+
|
|
480
|
+
```js
|
|
481
|
+
// SolidStart 2: register this module using solidStart({ middleware: ... }).
|
|
482
|
+
import { createMiddleware } from '@solidjs/start/middleware';
|
|
483
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
484
|
+
|
|
485
|
+
let check;
|
|
486
|
+
export default createMiddleware([
|
|
487
|
+
async (event, next) => {
|
|
488
|
+
check ??= createStopneticAppHandler((_request, state) => {
|
|
489
|
+
state.allowed = true;
|
|
490
|
+
return new Response(null, { status: 204 });
|
|
491
|
+
});
|
|
492
|
+
// Per-request state: a verification 204 must NEVER be mistaken for permission.
|
|
493
|
+
const state = { allowed: false };
|
|
494
|
+
const response = await check(event.req, state);
|
|
495
|
+
if (!state.allowed) return response;
|
|
496
|
+
event.res.headers.set('cache-control', 'private, no-store');
|
|
497
|
+
const result = await next(); // Preserve H3/SolidStart context and raw results.
|
|
498
|
+
if (!(result instanceof Response)) return result;
|
|
499
|
+
const headers = new Headers(result.headers);
|
|
500
|
+
headers.set('cache-control', 'private, no-store');
|
|
501
|
+
return new Response(result.body, {
|
|
502
|
+
status: result.status, statusText: result.statusText, headers,
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
]);
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
#### src/middleware.js — SolidStart 1 alternative
|
|
509
|
+
|
|
510
|
+
```js
|
|
511
|
+
// SolidStart 1: register using defineConfig({ middleware: './src/middleware.js' }).
|
|
512
|
+
// Keep the existing entry-server.tsx renderer. This is NOT SolidStart 2 config.
|
|
513
|
+
import { createMiddleware } from '@solidjs/start/middleware';
|
|
514
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
515
|
+
|
|
516
|
+
let check;
|
|
517
|
+
export default createMiddleware({
|
|
518
|
+
onRequest: async event => {
|
|
519
|
+
check ??= createStopneticAppHandler((_request, state) => {
|
|
520
|
+
state.allowed = true;
|
|
521
|
+
return new Response(null, { status: 204 });
|
|
522
|
+
});
|
|
523
|
+
const state = { allowed: false };
|
|
524
|
+
const response = await check(event.request, state);
|
|
525
|
+
if (!state.allowed) return response;
|
|
526
|
+
event.response.headers.set('cache-control', 'private, no-store');
|
|
527
|
+
// undefined continues to SolidStart; preserve its rendering and API handling.
|
|
528
|
+
},
|
|
529
|
+
});
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Framework references: [SolidStart 2 middleware](https://docs.solidjs.com/solid-start/v2/reference/server/create-middleware), [SolidStart 1 middleware](https://docs.solidjs.com/solid-start/v1/reference/server/create-middleware).
|
|
533
|
+
|
|
534
|
+
### Custom Node SSR or plain JavaScript server
|
|
535
|
+
|
|
536
|
+
**Where:** Existing root request entry, before static HTML, API data or rendering.
|
|
537
|
+
|
|
538
|
+
1. Use the Express helper OR the Fetch wrapper according to the existing server signature. Do not replace the renderer or introduce a second server.
|
|
539
|
+
2. Call installStopneticSSR(app) first for Express. For Fetch, export the protected version of the existing handler using protectSSR(renderApplication); all context arguments are retained.
|
|
540
|
+
3. Keep the canonical Host and private environment in the Node service. Only expose the guarded entry. Independently guard separately deployed API functions.
|
|
541
|
+
|
|
542
|
+
**Prerendering and caches:** Static HTML/cache aliases must not bypass the guard. Preserve existing public routes and apply protection only through dashboard policies.
|
|
543
|
+
|
|
544
|
+
**Hosting runtime:** Node.js 22+ only. Keep the existing Node hosting adapter; check the deployed runtime, not just the hosting brand. Edge/Deno deployments need a separate supported adapter or a gateway decision. Do not convert SSR to static rendering automatically.
|
|
545
|
+
|
|
546
|
+
#### stopnetic-ssr.mjs
|
|
547
|
+
|
|
548
|
+
```js
|
|
549
|
+
// Call before parsers, static files, APIs and your existing Node SSR renderer.
|
|
550
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
551
|
+
export function installStopneticSSR(app) {
|
|
552
|
+
let server;
|
|
553
|
+
app.use((req, res, next) => {
|
|
554
|
+
server ??= createStopneticServer();
|
|
555
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
#### stopnetic-fetch-ssr.mjs
|
|
561
|
+
|
|
562
|
+
```js
|
|
563
|
+
// Wrap your existing Node Request -> Response renderer; retain all context.
|
|
564
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
565
|
+
export function protectSSR(renderApplication) {
|
|
566
|
+
let guarded;
|
|
567
|
+
return (request, ...context) => {
|
|
568
|
+
guarded ??= createStopneticAppHandler(renderApplication);
|
|
569
|
+
return guarded(request, ...context);
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
<!-- END SSR EXAMPLES -->
|
|
136
575
|
|
|
137
576
|
### Static apps on Netlify / Vercel
|
|
138
577
|
|
|
@@ -148,7 +587,55 @@ root rather than `/assets`. Bundle the correct HTML and assets, preserve their
|
|
|
148
587
|
MIME types, and do not use the Vite-only file paths unchanged for Angular.
|
|
149
588
|
Reject direct entry-HTML and function aliases that would bypass protected paths.
|
|
150
589
|
|
|
151
|
-
## 4.
|
|
590
|
+
## 4. Navigation — server enforcement by default
|
|
591
|
+
|
|
592
|
+
For BOTH rendering options, leave the existing browser entry and app layout
|
|
593
|
+
alone. Use document links for page navigation so every target reaches the
|
|
594
|
+
guarded server before rendering. No global `checkAccess`, `StopneticBoundary`,
|
|
595
|
+
startup import gate, root access service or loading message is required.
|
|
596
|
+
|
|
597
|
+
| Frontend | Document navigation |
|
|
598
|
+
| --- | --- |
|
|
599
|
+
| Angular | `<a href="/members">Members</a>` instead of `routerLink` for this navigation |
|
|
600
|
+
| React | `<a href="/members">Members</a>` instead of router `<Link>` |
|
|
601
|
+
| Vue | `<a href="/members">Members</a>` instead of `<RouterLink>` |
|
|
602
|
+
| Svelte / SvelteKit | `<a href="/members" data-sveltekit-reload>Members</a>`; disable any custom interception |
|
|
603
|
+
| Solid | `<a href="/members" target="_self">Members</a>` instead of router `<A>` |
|
|
604
|
+
| Plain JavaScript | `<a href="/members">Members</a>`; do not intercept it with `pushState` |
|
|
605
|
+
|
|
606
|
+
Replace programmatic SPA transitions with `location.assign('/members')` when
|
|
607
|
+
using this mode. Turn off router prefetch/preload of protected page data that
|
|
608
|
+
would bypass the guarded document request. Existing routers may still resolve
|
|
609
|
+
the current URL after loading. Normal pages keep their usual loading behavior;
|
|
610
|
+
Stopnetic's verification HTML is sent only when the server decision requires it.
|
|
611
|
+
|
|
612
|
+
For SSR, hydrate the HTML already authorized by the server. Do not replace it
|
|
613
|
+
with a browser-only loading screen or run a redundant startup check. Private API
|
|
614
|
+
requests remain independently checked by their server handler.
|
|
615
|
+
|
|
616
|
+
### Optional SPA navigation — explicit asynchronous tradeoff
|
|
617
|
+
|
|
618
|
+
Retain SPA navigation only when needed. SPA transitions do not make a new
|
|
619
|
+
document request, so check each target in the router before showing its content.
|
|
620
|
+
The current browser API returns access after a server request; it does not
|
|
621
|
+
provide a synchronous dashboard route manifest. Public transitions also need
|
|
622
|
+
that check to discover the current policy. A brief empty pending state is
|
|
623
|
+
possible; it is not a challenge and is not advertised as instant public rendering.
|
|
624
|
+
Use document navigation above if this delay is unacceptable. Do not bypass
|
|
625
|
+
checks with a hardcoded public/protected list, cached `allowed` flag, or client
|
|
626
|
+
user-agent detection. Keep full target content hidden until the check succeeds.
|
|
627
|
+
|
|
628
|
+
The six optional demos below intentionally check their sample router targets.
|
|
629
|
+
They are not instructions to protect all those paths in Policies. Their pending
|
|
630
|
+
state contains no `Checking access...` text. They do not provide server SSR
|
|
631
|
+
integration or HTTP status changes. Do not install both a startup check and a
|
|
632
|
+
router gate; do not apply these browser demos to an existing hydrated SSR root.
|
|
633
|
+
|
|
634
|
+
### Optional browser startup gate for a browser-only app
|
|
635
|
+
|
|
636
|
+
This alternative is useful only if browser mounting must wait for an access
|
|
637
|
+
check. Skip it when the server already guards HTML and document navigation is
|
|
638
|
+
used. It is not required by either rendering option above.
|
|
152
639
|
|
|
153
640
|
For a browser-only/static app, move the existing startup code from `src/main.ts`
|
|
154
641
|
or `src/main.js` into `src/bootstrap.ts` or `src/bootstrap.js`. Keep its original
|
|
@@ -186,26 +673,26 @@ This is browser code, not a server hook. The startup file can use any framework:
|
|
|
186
673
|
| Solid | Your existing `render(...)` |
|
|
187
674
|
| Plain JavaScript | Your existing DOM rendering code |
|
|
188
675
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
does not rerun the startup file. If keeping SPA navigation, call `checkAccess`
|
|
192
|
-
in that router's before-navigation guard for every target and handle denial,
|
|
193
|
-
verification redirects, outages and stale/cancelled requests. A UI check cannot
|
|
194
|
-
set the original HTTP status or make public bundles private.
|
|
676
|
+
Client router navigation does not rerun this optional startup file. A UI check
|
|
677
|
+
cannot set the original HTTP status or make public bundles private.
|
|
195
678
|
|
|
196
679
|
<!-- BEGIN FRONTEND EXAMPLES -->
|
|
197
680
|
|
|
198
|
-
###
|
|
681
|
+
### Optional SPA demos — choose your frontend implementation
|
|
199
682
|
|
|
200
683
|
The package includes copyable source files in `examples/frontend`. The same
|
|
201
|
-
examples appear below and in the dashboard frontend selector.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
684
|
+
examples appear below and in the dashboard frontend selector. Skip this
|
|
685
|
+
section for the default server/document-navigation integration. These are
|
|
686
|
+
optional browser-only demos, not replacements for an existing app or SSR
|
|
687
|
+
entry. Retaining SPA navigation requires asynchronous target checks, even
|
|
688
|
+
for public targets. Pending content is empty; no global checking text is
|
|
689
|
+
required. Keep the target layout hidden until allowed to avoid exposing it
|
|
690
|
+
on denial. Only dashboard rules decide which paths require verification.
|
|
691
|
+
|
|
692
|
+
If explicitly choosing this mode, merge the relevant hooks into your own
|
|
693
|
+
browser router and adapt the sample paths. Do not add every demo path to
|
|
694
|
+
Policies. Do not combine these gates with the optional startup gate or
|
|
695
|
+
apply them to a hydrated SSR root. SSR uses section 3 and document navigation.
|
|
209
696
|
|
|
210
697
|
Copy the selected folder under `src/<frontend>/` and the shared data helper
|
|
211
698
|
to `src/load-private-data.js` (plus its `.d.ts` file for TypeScript). This keeps
|
|
@@ -261,7 +748,7 @@ import { loadPrivateData } from '../load-private-data.js';
|
|
|
261
748
|
function RouteGuard({ children }: { children: ReactNode }) {
|
|
262
749
|
const location = useLocation();
|
|
263
750
|
return <StopneticBoundary path={location.pathname + location.search}
|
|
264
|
-
pending={
|
|
751
|
+
pending={null}
|
|
265
752
|
blocked={<p>Access unavailable.</p>}>
|
|
266
753
|
{children}
|
|
267
754
|
</StopneticBoundary>;
|
|
@@ -314,7 +801,7 @@ createRoot(document.getElementById('app')!).render(<App/>);
|
|
|
314
801
|
|
|
315
802
|
### Angular Router
|
|
316
803
|
|
|
317
|
-
|
|
804
|
+
Optional browser-only SPA demo: the service records checking/allowed/denied state. Checks evaluate policy; only dashboard rules require verification. Members uses HttpClient after route access succeeds. Merge providers into your existing browser appConfig. Preserve SSR/hydration with the server/document-navigation option instead of applying this demo to a hydrated root. Modern standalone Angular syntax.
|
|
318
805
|
|
|
319
806
|
#### src/angular/app/app.config.ts
|
|
320
807
|
|
|
@@ -375,9 +862,7 @@ import { StopneticAccess } from './stopnetic-access.service';
|
|
|
375
862
|
</nav></header>
|
|
376
863
|
<router-outlet/>
|
|
377
864
|
<footer>Example site</footer>
|
|
378
|
-
} @else if (access.state() === '
|
|
379
|
-
<p role="status">Checking access...</p>
|
|
380
|
-
} @else {
|
|
865
|
+
} @else if (access.state() === 'denied') {
|
|
381
866
|
<p role="alert">Access unavailable.</p>
|
|
382
867
|
}
|
|
383
868
|
`,
|
|
@@ -478,8 +963,7 @@ import { accessState } from './router.js';
|
|
|
478
963
|
<RouterView/>
|
|
479
964
|
<footer>Example site</footer>
|
|
480
965
|
</template>
|
|
481
|
-
<p v-else-if="accessState === '
|
|
482
|
-
<p v-else role="alert">Access unavailable.</p>
|
|
966
|
+
<p v-else-if="accessState === 'denied'" role="alert">Access unavailable.</p>
|
|
483
967
|
</template>
|
|
484
968
|
```
|
|
485
969
|
|
|
@@ -620,9 +1104,7 @@ Svelte 5 example with onMount, History API navigation, popstate and abort cleanu
|
|
|
620
1104
|
</nav></header>
|
|
621
1105
|
<main><h1>{path === '/members' ? 'Members' : path === '/' ? 'Home' : path === '/slow' ? 'Slow' : 'Not found'}</h1><p>{data}</p></main>
|
|
622
1106
|
<footer>Example site</footer>
|
|
623
|
-
{:else if state === '
|
|
624
|
-
<p role="status">Checking access...</p>
|
|
625
|
-
{:else}
|
|
1107
|
+
{:else if state === 'denied'}
|
|
626
1108
|
<p role="alert">Access unavailable.</p>
|
|
627
1109
|
{/if}
|
|
628
1110
|
```
|
|
@@ -668,8 +1150,8 @@ const GuardedLayout: ParentComponent = props => {
|
|
|
668
1150
|
onCleanup(() => controller.abort());
|
|
669
1151
|
});
|
|
670
1152
|
return <Show when={result().allowed && result().path === path() ? result() : undefined}
|
|
671
|
-
fallback={<Show when={result().pending
|
|
672
|
-
|
|
1153
|
+
fallback={<Show when={!result().pending && result().path === path()}>
|
|
1154
|
+
<p role="alert">Access unavailable.</p></Show>}>
|
|
673
1155
|
{_checked => <>
|
|
674
1156
|
<header><nav><A href="/">Home</A>{' '}<A href="/members">Members</A>{' '}
|
|
675
1157
|
<A href="/slow">Slow</A>{' '}<A href="/blocked">Blocked</A>{' '}<A href="/offline">Offline</A>
|
|
@@ -725,7 +1207,7 @@ let current;
|
|
|
725
1207
|
async function showPage() {
|
|
726
1208
|
current?.abort();
|
|
727
1209
|
const controller = new AbortController(); current = controller;
|
|
728
|
-
root.replaceChildren(
|
|
1210
|
+
root.replaceChildren();
|
|
729
1211
|
try {
|
|
730
1212
|
const path = location.pathname;
|
|
731
1213
|
const result = await checkAccess(path + location.search, { signal: controller.signal });
|
|
@@ -810,6 +1292,14 @@ client navigation, entry HTML, aliases, prerender outputs and caches. A denied
|
|
|
810
1292
|
request must not execute the renderer or data handler. The offline CLI check
|
|
811
1293
|
validates configuration and generated files, not deployed coverage.
|
|
812
1294
|
|
|
1295
|
+
With only `/members` protected, confirm `/about` and `/contact` render without
|
|
1296
|
+
a Stopnetic checking screen. Change a dashboard rule and make a new document
|
|
1297
|
+
request: the change must apply without editing a client route list. For SSR,
|
|
1298
|
+
confirm permitted HTML contains server-rendered page content before JavaScript
|
|
1299
|
+
runs, hydration still works, and the renderer is never called on denial. Verify
|
|
1300
|
+
the deployed request actually reaches Node; an SSR build alone proves nothing
|
|
1301
|
+
about a provider's deployed runtime. Optional SPA checks may have a pending delay.
|
|
1302
|
+
|
|
813
1303
|
Ordinary policy changes apply on subsequent guarded requests without reinstalling
|
|
814
1304
|
or redeploying. Already loaded content is not recalled. Public HTML/JS/assets and
|
|
815
1305
|
content bundled into frontend code remain public; keep private data in guarded
|
|
@@ -818,7 +1308,7 @@ server endpoints.
|
|
|
818
1308
|
## Updating an existing generated guide
|
|
819
1309
|
|
|
820
1310
|
After installing the new SDK, rerun `stopnetic-js init --platform <same-platform>`
|
|
821
|
-
(preview with `--dry-run`). An unchanged 0.0.2/0.0.3 generated guide is upgraded
|
|
1311
|
+
(preview with `--dry-run`). An unchanged 0.0.2/0.0.3/0.0.4/0.0.5 generated guide is upgraded
|
|
822
1312
|
with its setup manifest. Your key, cookie secret and server files are preserved.
|
|
823
1313
|
Customized guides or handlers require manual merging; switching platforms also
|
|
824
1314
|
requires review. This command does not publish, deploy or contact Stopnetic.
|
package/dist/setup.js
CHANGED
|
@@ -38,6 +38,8 @@ function platformFor(root, supplied) {
|
|
|
38
38
|
const legacyGuideDigests = new Set([
|
|
39
39
|
'b1f149af44f9ceb08fb80c18cb5d4b521620196f536d8ad422ab94d15d1d125d', // 0.0.2
|
|
40
40
|
'55e36295b6aa87ad9558cde29786998483095adeae74e149ff5c7fe78507fad7', // published 0.0.3
|
|
41
|
+
'08f919257d8c85af4e56e842705825832ae2810ed68d5c1dfc8405c8f7be7610', // published 0.0.4
|
|
42
|
+
'45bd696f196c3d1447a978987a787755b239f75e9e6a3697f4efbb210ce75597', // published 0.0.5
|
|
41
43
|
]);
|
|
42
44
|
const clientGuide = readFileSync(new URL('./client-guide.md', import.meta.url), 'utf8');
|
|
43
45
|
|
|
@@ -119,5 +121,5 @@ export function checkSetup(input, { localHttp = false } = {}) {
|
|
|
119
121
|
if (!localHttp && !options.apiUrl?.startsWith('https:')) throw new Error();
|
|
120
122
|
createStopneticAccessHandler(options);
|
|
121
123
|
} catch { throw new SetupError('Check server settings, HTTPS URLs, installation/signing keys and the separate cookie secret. No secret values are displayed.'); }
|
|
122
|
-
return `Checked ${manifest.platform} generated files and server configuration offline. Confirm
|
|
124
|
+
return `Checked ${manifest.platform} generated files and server configuration offline. Confirm server page enforcement, your chosen navigation mode, API wrappers and hosting routing with a deployed test. Preserve existing SSR; browser gates are optional. This does not prove coverage or start a trial.`;
|
|
123
125
|
}
|
|
@@ -13,9 +13,7 @@ import { StopneticAccess } from './stopnetic-access.service';
|
|
|
13
13
|
</nav></header>
|
|
14
14
|
<router-outlet/>
|
|
15
15
|
<footer>Example site</footer>
|
|
16
|
-
} @else if (access.state() === '
|
|
17
|
-
<p role="status">Checking access...</p>
|
|
18
|
-
} @else {
|
|
16
|
+
} @else if (access.state() === 'denied') {
|
|
19
17
|
<p role="alert">Access unavailable.</p>
|
|
20
18
|
}
|
|
21
19
|
`,
|
|
@@ -6,7 +6,7 @@ import { loadPrivateData } from '../load-private-data.js';
|
|
|
6
6
|
function RouteGuard({ children }: { children: ReactNode }) {
|
|
7
7
|
const location = useLocation();
|
|
8
8
|
return <StopneticBoundary path={location.pathname + location.search}
|
|
9
|
-
pending={
|
|
9
|
+
pending={null}
|
|
10
10
|
blocked={<p>Access unavailable.</p>}>
|
|
11
11
|
{children}
|
|
12
12
|
</StopneticBoundary>;
|
|
@@ -21,8 +21,8 @@ const GuardedLayout: ParentComponent = props => {
|
|
|
21
21
|
onCleanup(() => controller.abort());
|
|
22
22
|
});
|
|
23
23
|
return <Show when={result().allowed && result().path === path() ? result() : undefined}
|
|
24
|
-
fallback={<Show when={result().pending
|
|
25
|
-
|
|
24
|
+
fallback={<Show when={!result().pending && result().path === path()}>
|
|
25
|
+
<p role="alert">Access unavailable.</p></Show>}>
|
|
26
26
|
{_checked => <>
|
|
27
27
|
<header><nav><A href="/">Home</A>{' '}<A href="/members">Members</A>{' '}
|
|
28
28
|
<A href="/slow">Slow</A>{' '}<A href="/blocked">Blocked</A>{' '}<A href="/offline">Offline</A>
|
|
@@ -51,8 +51,6 @@
|
|
|
51
51
|
</nav></header>
|
|
52
52
|
<main><h1>{path === '/members' ? 'Members' : path === '/' ? 'Home' : path === '/slow' ? 'Slow' : 'Not found'}</h1><p>{data}</p></main>
|
|
53
53
|
<footer>Example site</footer>
|
|
54
|
-
{:else if state === '
|
|
55
|
-
<p role="status">Checking access...</p>
|
|
56
|
-
{:else}
|
|
54
|
+
{:else if state === 'denied'}
|
|
57
55
|
<p role="alert">Access unavailable.</p>
|
|
58
56
|
{/if}
|
|
@@ -5,7 +5,7 @@ let current;
|
|
|
5
5
|
async function showPage() {
|
|
6
6
|
current?.abort();
|
|
7
7
|
const controller = new AbortController(); current = controller;
|
|
8
|
-
root.replaceChildren(
|
|
8
|
+
root.replaceChildren();
|
|
9
9
|
try {
|
|
10
10
|
const path = location.pathname;
|
|
11
11
|
const result = await checkAccess(path + location.search, { signal: controller.signal });
|
|
@@ -13,6 +13,5 @@ import { accessState } from './router.js';
|
|
|
13
13
|
<RouterView/>
|
|
14
14
|
<footer>Example site</footer>
|
|
15
15
|
</template>
|
|
16
|
-
<p v-else-if="accessState === '
|
|
17
|
-
<p v-else role="alert">Access unavailable.</p>
|
|
16
|
+
<p v-else-if="accessState === 'denied'" role="alert">Access unavailable.</p>
|
|
18
17
|
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Merge into your existing Node SSR entry; retain host settings and context.
|
|
2
|
+
import { AngularAppEngine, createRequestHandler } from '@angular/ssr';
|
|
3
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
4
|
+
|
|
5
|
+
const engine = new AngularAppEngine();
|
|
6
|
+
let guarded: ReturnType<typeof createStopneticAppHandler> | undefined;
|
|
7
|
+
async function handle(request: Request): Promise<Response> {
|
|
8
|
+
guarded ??= createStopneticAppHandler(async checked =>
|
|
9
|
+
await engine.handle(checked) ?? new Response(null, { status: 404 }));
|
|
10
|
+
return guarded(request);
|
|
11
|
+
}
|
|
12
|
+
export const reqHandler = createRequestHandler(handle);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/middleware.js. Keep the existing Node deployment adapter.
|
|
2
|
+
import { defineMiddleware } from 'astro:middleware';
|
|
3
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
4
|
+
|
|
5
|
+
let guarded;
|
|
6
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
7
|
+
// Public prerender builds only; protected pages must export prerender=false.
|
|
8
|
+
if (context.isPrerendered) return next();
|
|
9
|
+
guarded ??= createStopneticAppHandler((_request, run) => run());
|
|
10
|
+
return guarded(context.request, () => next());
|
|
11
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Wrap your existing Node Request -> Response renderer; retain all context.
|
|
2
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
3
|
+
export function protectSSR(renderApplication) {
|
|
4
|
+
let guarded;
|
|
5
|
+
return (request, ...context) => {
|
|
6
|
+
guarded ??= createStopneticAppHandler(renderApplication);
|
|
7
|
+
return guarded(request, ...context);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Call before parsers, static files, APIs and your existing Node SSR renderer.
|
|
2
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
3
|
+
export function installStopneticSSR(app) {
|
|
4
|
+
let server;
|
|
5
|
+
app.use((req, res, next) => {
|
|
6
|
+
server ??= createStopneticServer();
|
|
7
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// server/middleware/00-stopnetic.mjs. Nuxt 3/4, Nitro 2, H3 1, NODE only.
|
|
2
|
+
// Use server middleware, not app/middleware or defineNuxtRouteMiddleware.
|
|
3
|
+
import { fromNodeMiddleware } from 'h3';
|
|
4
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
5
|
+
|
|
6
|
+
let server;
|
|
7
|
+
export default fromNodeMiddleware((req, res, next) => {
|
|
8
|
+
server ??= createStopneticServer();
|
|
9
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Existing custom React SSR server: call before assets, APIs and React rendering.
|
|
2
|
+
// Keep renderToPipeableStream/renderToReadableStream and hydration unchanged.
|
|
3
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
4
|
+
export function installStopneticReactSSR(app) {
|
|
5
|
+
let server;
|
|
6
|
+
app.use((req, res, next) => {
|
|
7
|
+
server ??= createStopneticServer();
|
|
8
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Call installStopneticReactRouter(app) before parsers, assets and the existing
|
|
2
|
+
// @react-router/express createRequestHandler. Keep that handler/context intact.
|
|
3
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
4
|
+
|
|
5
|
+
export function installStopneticReactRouter(app) {
|
|
6
|
+
let server;
|
|
7
|
+
app.use((req, res, next) => {
|
|
8
|
+
server ??= createStopneticServer();
|
|
9
|
+
return server.access(req, res, () => {
|
|
10
|
+
// React Router 7 single-fetch data URLs must use the page's policy.
|
|
11
|
+
// The renderer still receives the original request and unread body.
|
|
12
|
+
const url = new URL(req.originalUrl || req.url, 'https://example.invalid');
|
|
13
|
+
let checked = req;
|
|
14
|
+
if (url.pathname.endsWith('.data')) {
|
|
15
|
+
url.pathname = url.pathname === '/_root.data' ? '/' : url.pathname.slice(0, -5);
|
|
16
|
+
checked = Object.create(req);
|
|
17
|
+
checked.originalUrl = url.pathname + url.search;
|
|
18
|
+
}
|
|
19
|
+
return server.guard(checked, res, next);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SolidStart 1: register using defineConfig({ middleware: './src/middleware.js' }).
|
|
2
|
+
// Keep the existing entry-server.tsx renderer. This is NOT SolidStart 2 config.
|
|
3
|
+
import { createMiddleware } from '@solidjs/start/middleware';
|
|
4
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
5
|
+
|
|
6
|
+
let check;
|
|
7
|
+
export default createMiddleware({
|
|
8
|
+
onRequest: async event => {
|
|
9
|
+
check ??= createStopneticAppHandler((_request, state) => {
|
|
10
|
+
state.allowed = true;
|
|
11
|
+
return new Response(null, { status: 204 });
|
|
12
|
+
});
|
|
13
|
+
const state = { allowed: false };
|
|
14
|
+
const response = await check(event.request, state);
|
|
15
|
+
if (!state.allowed) return response;
|
|
16
|
+
event.response.headers.set('cache-control', 'private, no-store');
|
|
17
|
+
// undefined continues to SolidStart; preserve its rendering and API handling.
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SolidStart 2: register this module using solidStart({ middleware: ... }).
|
|
2
|
+
import { createMiddleware } from '@solidjs/start/middleware';
|
|
3
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
4
|
+
|
|
5
|
+
let check;
|
|
6
|
+
export default createMiddleware([
|
|
7
|
+
async (event, next) => {
|
|
8
|
+
check ??= createStopneticAppHandler((_request, state) => {
|
|
9
|
+
state.allowed = true;
|
|
10
|
+
return new Response(null, { status: 204 });
|
|
11
|
+
});
|
|
12
|
+
// Per-request state: a verification 204 must NEVER be mistaken for permission.
|
|
13
|
+
const state = { allowed: false };
|
|
14
|
+
const response = await check(event.req, state);
|
|
15
|
+
if (!state.allowed) return response;
|
|
16
|
+
event.res.headers.set('cache-control', 'private, no-store');
|
|
17
|
+
const result = await next(); // Preserve H3/SolidStart context and raw results.
|
|
18
|
+
if (!(result instanceof Response)) return result;
|
|
19
|
+
const headers = new Headers(result.headers);
|
|
20
|
+
headers.set('cache-control', 'private, no-store');
|
|
21
|
+
return new Response(result.body, {
|
|
22
|
+
status: result.status, statusText: result.statusText, headers,
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { building } from '$app/environment';
|
|
2
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
3
|
+
|
|
4
|
+
let guarded;
|
|
5
|
+
// When merging existing hooks, run this first and put them in the continuation.
|
|
6
|
+
export async function handle({ event, resolve }) {
|
|
7
|
+
// Build-time public prerendering only. Protected routes must have prerender=false.
|
|
8
|
+
if (building) return resolve(event);
|
|
9
|
+
guarded ??= createStopneticAppHandler((_request, run) => run());
|
|
10
|
+
// SvelteKit exposes the canonical page URL on data requests. Check that page
|
|
11
|
+
// rather than allowing an unmatched /members/__data.json alias through.
|
|
12
|
+
const checked = event.isDataRequest
|
|
13
|
+
? new Request(event.url, { method: event.request.method, headers: event.request.headers })
|
|
14
|
+
: event.request;
|
|
15
|
+
return guarded(checked, () => resolve(event));
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Existing custom Vue SSR server: call before assets, APIs and Vue rendering.
|
|
2
|
+
// Keep createSSRApp/renderToString and client hydration unchanged.
|
|
3
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
4
|
+
export function installStopneticVueSSR(app) {
|
|
5
|
+
let server;
|
|
6
|
+
app.use((req, res, next) => {
|
|
7
|
+
server ??= createStopneticServer();
|
|
8
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
9
|
+
});
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stopnetic/js",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6-pilot",
|
|
4
4
|
"description": "Server-side Stopnetic guard for JavaScript websites, Node.js and Express-compatible servers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
22
|
"bin",
|
|
23
|
-
"examples/frontend"
|
|
23
|
+
"examples/frontend",
|
|
24
|
+
"examples/ssr"
|
|
24
25
|
],
|
|
25
26
|
"bin": { "stopnetic-js": "./bin/stopnetic-js.js" },
|
|
26
27
|
"exports": {
|