@stopnetic/js 0.0.2-pilot → 0.0.3-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 +58 -8
- package/dist/client-guide.md +276 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +16 -0
- package/dist/setup.js +13 -59
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,12 +6,18 @@ 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
|
-
|
|
9
|
+
Version **0.0.3-pilot is prepared locally; it is not published yet**. It adds a
|
|
10
|
+
framework-independent generated guide, a complete Node application wrapper
|
|
11
|
+
(`createStopneticAppHandler`) and safe migration of the unchanged older guide.
|
|
12
|
+
Angular, React, Vue, Svelte, Solid and plain JavaScript share the browser/server
|
|
13
|
+
contracts; no React dependency is required. See the
|
|
14
|
+
[complete generated guide](src/client-guide.md) for SSR, static builds, Angular,
|
|
15
|
+
client navigation, API protection and runtime restrictions.
|
|
16
|
+
|
|
17
|
+
The currently published **0.0.2-pilot** adds a reusable browser/server setup for Netlify, Vercel
|
|
11
18
|
and Node/Express, a browser access check and optional React boundary. It also
|
|
12
19
|
normalizes public PEM formatting and rejects placeholder or reused installation
|
|
13
|
-
keys as cookie secrets.
|
|
14
|
-
currently published manual server pilot with:
|
|
20
|
+
keys as cookie secrets. Install the published pilot with:
|
|
15
21
|
|
|
16
22
|
```sh
|
|
17
23
|
npm install @stopnetic/js@pilot
|
|
@@ -25,15 +31,20 @@ npm run build
|
|
|
25
31
|
npm pack
|
|
26
32
|
```
|
|
27
33
|
|
|
28
|
-
Install the resulting `stopnetic-js-0.0.
|
|
34
|
+
Install the resulting `stopnetic-js-0.0.3-pilot.tgz` into the website project using
|
|
29
35
|
`npm install <absolute-path-to-tarball>`. The installed package has no Next.js or
|
|
30
36
|
other runtime dependency. The build packages the same signed decision implementation
|
|
31
37
|
as the Next.js adapter. Keep `@stopnetic/next` for its Next.js 16 guided installer.
|
|
32
38
|
|
|
33
39
|
## Reusable client/server setup (requires 0.0.2-pilot)
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
Follow the [hosting guide](../../docs/javascript-hosting-guide.md) for the full
|
|
42
|
+
Netlify, Vercel or Node procedure, server settings, page HTTP 404 enforcement,
|
|
43
|
+
frontend/API wiring and diagnosis. The dashboard's Integration and Documentation
|
|
44
|
+
pages offer the same provider choices and complete copyable page-guard examples.
|
|
45
|
+
|
|
46
|
+
The published 0.0.2-pilot includes these commands and subpath exports.
|
|
47
|
+
Older 0.0.1-pilot installations must upgrade before using them.
|
|
37
48
|
|
|
38
49
|
Run setup at the app's package.json, choose hosting, and preview before writing:
|
|
39
50
|
|
|
@@ -67,7 +78,31 @@ and server functions; Vite alone does not provide the generated server endpoints
|
|
|
67
78
|
For Node, use `node --env-file=.env.local your-server.mjs` or your existing
|
|
68
79
|
server environment loader. Production credentials belong in runtime settings.
|
|
69
80
|
|
|
70
|
-
###
|
|
81
|
+
### Shared browser startup and Node SSR
|
|
82
|
+
|
|
83
|
+
The generated [framework-independent guide](src/client-guide.md) gives one
|
|
84
|
+
browser entry that calls `checkAccess` before importing the application's
|
|
85
|
+
existing bootstrap. Keep Angular's bootstrapApplication, React's createRoot,
|
|
86
|
+
Vue's createApp, Svelte's mount, Solid's render or plain DOM startup unchanged
|
|
87
|
+
in that bootstrap module. Use document links for server-checked navigation,
|
|
88
|
+
or recheck target access in the client router. A browser-only check is UI behavior.
|
|
89
|
+
|
|
90
|
+
For Node SSR use the generated Express helper before static files/rendering,
|
|
91
|
+
or `createStopneticAppHandler` around a Fetch-style application entry (0.0.3).
|
|
92
|
+
The wrapper handles access/verification control endpoints and guards pages and
|
|
93
|
+
APIs before calling the renderer. It preserves context, streams and request
|
|
94
|
+
bodies. Framework hooks with another signature still need their continuation
|
|
95
|
+
adapted; accepting Request/Response does not make an Edge runtime supported.
|
|
96
|
+
|
|
97
|
+
The guide includes Angular SSR/rendering and client router examples. Angular
|
|
98
|
+
SSR compiled with the installed Angular 22 dependencies and served real HTML
|
|
99
|
+
after verification in an isolated local fixture. That does not validate a live
|
|
100
|
+
provider deployment. Netlify's automatic Angular SSR uses Edge; choose Node SSR,
|
|
101
|
+
a Node gateway or a deliberate static build. Do not publish protected prerendered
|
|
102
|
+
HTML. For other frameworks use the same Node server entry and verify their
|
|
103
|
+
actual deployed runtime. React's boundary remains an optional convenience.
|
|
104
|
+
|
|
105
|
+
### Optional React Router boundary
|
|
71
106
|
|
|
72
107
|
The optional React entry uses Hooks. The core/browser modules need no React.
|
|
73
108
|
Place this component inside BrowserRouter, wrapping your Routes:
|
|
@@ -119,6 +154,21 @@ content or an API authorization token. API wrappers independently check every
|
|
|
119
154
|
request and preserve ordinary request bodies. Denials/outages return empty
|
|
120
155
|
404/503 responses; approved data responses use `private, no-store`.
|
|
121
156
|
|
|
157
|
+
For a real HTTP 404 on a Netlify SPA page, use the optional page-serving function
|
|
158
|
+
in `examples/netlify-spa-pages.mjs` and follow
|
|
159
|
+
[`docs/netlify-spa-page-requests.md`](../../docs/netlify-spa-page-requests.md).
|
|
160
|
+
It checks signed access before reading the bundled entry HTML. The browser
|
|
161
|
+
boundary alone cannot change the initial document's HTTP status. This example
|
|
162
|
+
uses the published 0.0.2-pilot server helpers; the setup CLI does not install it
|
|
163
|
+
automatically. Keep static bundles public and private data in guarded APIs.
|
|
164
|
+
|
|
165
|
+
For Vercel page responses, the manual Node routing middleware in
|
|
166
|
+
`examples/vercel-spa-middleware.mjs` checks pages before static HTML, using
|
|
167
|
+
`@vercel/functions` and the existing SDK. The CLI's generated Vercel control
|
|
168
|
+
function alone does not guard page documents. Follow the hosting guide for
|
|
169
|
+
middleware, API/asset routing and SPA rewrite configuration. Local signed tests
|
|
170
|
+
cover it; live Vercel deployment validation remains required.
|
|
171
|
+
|
|
122
172
|
### Node servers on other hosts
|
|
123
173
|
|
|
124
174
|
Import `installStopnetic` from generated `stopnetic.server.mjs`, then call
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# Stopnetic setup for JavaScript applications
|
|
2
|
+
|
|
3
|
+
Angular, React, Vue, Svelte, Solid and plain JavaScript use the same server guard
|
|
4
|
+
and browser access check. React is optional. Install `@stopnetic/js` in the
|
|
5
|
+
website project; choose a hosting platform, not a frontend framework.
|
|
6
|
+
|
|
7
|
+
The generated control endpoint is only part of installation. It does not
|
|
8
|
+
automatically intercept pages, edit your router or protect existing APIs.
|
|
9
|
+
|
|
10
|
+
## 1. Choose the actual rendering and server runtime
|
|
11
|
+
|
|
12
|
+
| Application | Where the shared guard belongs |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| Node/Express SSR or static-file server | Before body parsers, static files, API handlers and rendering |
|
|
15
|
+
| Node server with a Fetch `Request` → `Response` handler | Wrap the complete application handler before it renders or returns API data |
|
|
16
|
+
| Static Angular, React, Vue, Svelte, Solid or plain HTML | A Node hosting function/middleware before HTML, plus the shared browser check below |
|
|
17
|
+
| Angular SSR, Nuxt, SvelteKit, Astro, SolidStart, React Router server mode | The deployed Node request entry, before the framework renderer; preserve framework context |
|
|
18
|
+
| Next.js | Use `@stopnetic/next` and its Next.js installation guide |
|
|
19
|
+
| Edge/Deno/Cloudflare Workers, PHP, Python, Ruby, C# or Java | This Node SDK cannot be imported there; use a Node gateway or an adapter for that runtime |
|
|
20
|
+
|
|
21
|
+
Node.js 22 or newer is required. SSR support alone does not establish runtime
|
|
22
|
+
compatibility. A Fetch API on an Edge runtime is not a Node server.
|
|
23
|
+
Prerendering runs at build time: it is not a request-time access check. Do not
|
|
24
|
+
publish protected prerendered pages or private data to a public static directory.
|
|
25
|
+
|
|
26
|
+
## 2. Save the six server environment settings
|
|
27
|
+
|
|
28
|
+
Copy actual values from Stopnetic Integration into your hosting SERVER
|
|
29
|
+
environment. Use `https://www.example.com` only as an example; replace it with
|
|
30
|
+
your website's canonical origin.
|
|
31
|
+
|
|
32
|
+
```dotenv
|
|
33
|
+
STOPNETIC_WEBSITE_ORIGIN=https://www.example.com
|
|
34
|
+
STOPNETIC_SITE_ID=your_site_id
|
|
35
|
+
STOPNETIC_API_URL=https://your-stopnetic-service/api/integration/decision
|
|
36
|
+
STOPNETIC_API_TOKEN=your_private_installation_key
|
|
37
|
+
STOPNETIC_DECISION_PUBLIC_KEY=your_complete_public_PEM
|
|
38
|
+
STOPNETIC_COOKIE_SECRET=your_separate_random_cookie_secret
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The installer creates a private cookie secret in `.env.local` when missing.
|
|
42
|
+
Reuse it across deployments. Do not commit `.env.local`. Copy complete PEM
|
|
43
|
+
header/body/footer with real newlines or literal `\n`; quote multiline values
|
|
44
|
+
in local env files. Hosting value fields do not need surrounding quotes.
|
|
45
|
+
Never put private settings in frontend environment files or prefix them with
|
|
46
|
+
`VITE_`, `REACT_APP_` or `NEXT_PUBLIC_`. Angular's `environment.ts` is browser
|
|
47
|
+
configuration, not a private server environment.
|
|
48
|
+
|
|
49
|
+
## 3. Protect the server before rendering
|
|
50
|
+
|
|
51
|
+
### Node/Express: every frontend uses the same mount
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import { installStopnetic } from './stopnetic.server.mjs';
|
|
55
|
+
installStopnetic(app);
|
|
56
|
+
// Existing body parsers, static files, APIs and framework renderer follow.
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Generate that helper with `stopnetic-js init --platform node`. Only expose the
|
|
60
|
+
guarded server. Keep the control endpoints `/api/stopnetic/access` and
|
|
61
|
+
`/api/stopnetic/verify` reachable before SPA fallbacks. Preserve the canonical
|
|
62
|
+
Host at your proxy. This works without a React dependency or browser boundary.
|
|
63
|
+
|
|
64
|
+
### Node Fetch application: one wrapper for pages and APIs
|
|
65
|
+
|
|
66
|
+
In SDK 0.0.3-pilot or newer:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
70
|
+
|
|
71
|
+
let protectedApp;
|
|
72
|
+
export async function handleRequest(request, context) {
|
|
73
|
+
protectedApp ??= createStopneticAppHandler(async (checked, frameworkContext) => {
|
|
74
|
+
// Your existing Node framework handler. Return a Response.
|
|
75
|
+
return renderApplication(checked, frameworkContext);
|
|
76
|
+
});
|
|
77
|
+
return protectedApp(request, context);
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Lazy initialization reads environment settings at runtime, not during a
|
|
82
|
+
prerender build. Control requests are handled by Stopnetic. Other requests are
|
|
83
|
+
checked before the application handler runs; denied responses do not render
|
|
84
|
+
the site's navigation or private API data. Context and unread request bodies
|
|
85
|
+
are preserved. Continue enforcing your application's own user/data permissions.
|
|
86
|
+
|
|
87
|
+
For a framework hook, keep its existing request/context conventions and call
|
|
88
|
+
the shared guard before its `resolve`, `next` or renderer. A Fetch wrapper is
|
|
89
|
+
for a complete handler returning a Response, not every framework middleware
|
|
90
|
+
signature. Node/Express can mount the generated helper around any renderer.
|
|
91
|
+
|
|
92
|
+
### Angular SSR on Node
|
|
93
|
+
|
|
94
|
+
For a Fetch-style Angular SSR entry in `src/server.ts`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { AngularAppEngine, createRequestHandler } from '@angular/ssr';
|
|
98
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
99
|
+
|
|
100
|
+
const engine = new AngularAppEngine();
|
|
101
|
+
let guarded: ReturnType<typeof createStopneticAppHandler> | undefined;
|
|
102
|
+
async function handle(request: Request): Promise<Response> {
|
|
103
|
+
guarded ??= createStopneticAppHandler(async checked =>
|
|
104
|
+
await engine.handle(checked) ?? new Response(null, { status: 404 }));
|
|
105
|
+
return guarded(request);
|
|
106
|
+
}
|
|
107
|
+
export const reqHandler = createRequestHandler(handle);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Keep existing Angular host configuration and request context if your entry
|
|
111
|
+
uses them. For an Express-based Angular entry, mount `installStopnetic(app)`
|
|
112
|
+
before `express.static` and `AngularNodeAppEngine.handle` instead.
|
|
113
|
+
|
|
114
|
+
Protected routes in `src/app/app.routes.server.ts` must use request-time
|
|
115
|
+
rendering rather than `RenderMode.Prerender`:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { RenderMode, ServerRoute } from '@angular/ssr';
|
|
119
|
+
export const serverRoutes: ServerRoute[] = [
|
|
120
|
+
// All routes are rendered on incoming requests in this example.
|
|
121
|
+
{ path: '**', renderMode: RenderMode.Server },
|
|
122
|
+
];
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Netlify's automatic Angular SSR adapter currently uses Edge Functions. Do not
|
|
126
|
+
import this Node SDK into that Edge entry, and do not assume a Netlify Node
|
|
127
|
+
function runs before the Edge renderer. Use a Node SSR deployment, a Node
|
|
128
|
+
gateway that cannot be bypassed, or a static Angular build with Node page
|
|
129
|
+
handling. Switching to static rendering changes SSR behavior; choose deliberately.
|
|
130
|
+
|
|
131
|
+
### Static apps on Netlify / Vercel
|
|
132
|
+
|
|
133
|
+
The generated Netlify control file serves `/api/stopnetic/access` and
|
|
134
|
+
`/api/stopnetic/verify`; Vercel uses `api/stopnetic/[operation].mjs`. They do not
|
|
135
|
+
guard the original page request by themselves. Add a Node page function or
|
|
136
|
+
Node routing middleware before serving HTML if you need real page HTTP 404s.
|
|
137
|
+
The full hosting guide is at your Stopnetic dashboard's `/docs#javascript`.
|
|
138
|
+
|
|
139
|
+
Use your actual build output. Vite often uses `dist`; Angular commonly uses
|
|
140
|
+
`dist/<project>/browser`. Angular scripts/styles may be at that directory's
|
|
141
|
+
root rather than `/assets`. Bundle the correct HTML and assets, preserve their
|
|
142
|
+
MIME types, and do not use the Vite-only file paths unchanged for Angular.
|
|
143
|
+
Reject direct entry-HTML and function aliases that would bypass protected paths.
|
|
144
|
+
|
|
145
|
+
## 4. Browser startup: the same code for every frontend
|
|
146
|
+
|
|
147
|
+
For a browser-only/static app, move the existing startup code from `src/main.ts`
|
|
148
|
+
or `src/main.js` into `src/bootstrap.ts` or `src/bootstrap.js`. Keep its original
|
|
149
|
+
framework imports and bootstrap code. Replace the browser entry with:
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
import { checkAccess } from '@stopnetic/js/browser';
|
|
153
|
+
|
|
154
|
+
async function start() {
|
|
155
|
+
try {
|
|
156
|
+
const result = await checkAccess(location.pathname + location.search);
|
|
157
|
+
if (result.allowed) {
|
|
158
|
+
await import('./bootstrap'); // Your existing app startup, unchanged.
|
|
159
|
+
} else if (result.reason === 'verification_required' && result.verificationUrl) {
|
|
160
|
+
location.replace(result.verificationUrl);
|
|
161
|
+
} else {
|
|
162
|
+
document.body.replaceChildren(document.createTextNode(
|
|
163
|
+
result.reason === 'blocked' ? 'Not found.' : 'Access unavailable.'));
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
document.body.replaceChildren(document.createTextNode('Access unavailable.'));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
void start();
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
This is browser code, not a server hook. The startup file can use any framework:
|
|
173
|
+
|
|
174
|
+
| Frontend | Existing startup retained in `bootstrap` |
|
|
175
|
+
| --- | --- |
|
|
176
|
+
| Angular | `bootstrapApplication(App, appConfig)` or your existing NgModule bootstrap |
|
|
177
|
+
| React | `createRoot(...).render(...)` |
|
|
178
|
+
| Vue | `createApp(...).use(router).mount(...)` |
|
|
179
|
+
| Svelte | Your existing `mount(...)` or older constructor |
|
|
180
|
+
| Solid | Your existing `render(...)` |
|
|
181
|
+
| Plain JavaScript | Your existing DOM rendering code |
|
|
182
|
+
|
|
183
|
+
Use ordinary `<a href>` document links for guarded navigation: each navigation
|
|
184
|
+
then reaches the server before a new page is shown. Client router navigation
|
|
185
|
+
does not rerun the startup file. If keeping SPA navigation, call `checkAccess`
|
|
186
|
+
in that router's before-navigation guard for every target and handle denial,
|
|
187
|
+
verification redirects, outages and stale/cancelled requests. A UI check cannot
|
|
188
|
+
set the original HTTP status or make public bundles private.
|
|
189
|
+
|
|
190
|
+
### Angular client router, if retaining routerLink
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
194
|
+
import { inject, PLATFORM_ID } from '@angular/core';
|
|
195
|
+
import { CanActivateFn } from '@angular/router';
|
|
196
|
+
import { checkAccess } from '@stopnetic/js/browser';
|
|
197
|
+
|
|
198
|
+
export const stopneticRouteGuard: CanActivateFn = async (_route, state) => {
|
|
199
|
+
if (!isPlatformBrowser(inject(PLATFORM_ID))) return true;
|
|
200
|
+
// SSR is authorized by the SERVER wrapper above, never by this true value.
|
|
201
|
+
const result = await checkAccess(state.url.split('#')[0]);
|
|
202
|
+
if (result.allowed) return true;
|
|
203
|
+
if (result.reason === 'verification_required' && result.verificationUrl) location.assign(result.verificationUrl);
|
|
204
|
+
return false;
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Apply `canActivate: [stopneticRouteGuard]` and
|
|
209
|
+
`runGuardsAndResolvers: 'always'` on each page route, including root, or use an
|
|
210
|
+
equivalent parent `canActivateChild` guard. Keep the full header/router/footer
|
|
211
|
+
inside the gated layout. Render a visible unavailable/denied state when a
|
|
212
|
+
navigation is cancelled; returning false alone retains the previous view.
|
|
213
|
+
This guard is UX only. It does not authorize API data or replace server checks.
|
|
214
|
+
|
|
215
|
+
React users may additionally use `StopneticBoundary` from `@stopnetic/js/react`
|
|
216
|
+
inside BrowserRouter around the entire layout; that optional module is not
|
|
217
|
+
required for Angular, Vue, Svelte, Solid or plain JavaScript.
|
|
218
|
+
|
|
219
|
+
## 5. Protect APIs independently
|
|
220
|
+
|
|
221
|
+
If the full application wrapper is in front of an API, it checks that API too.
|
|
222
|
+
Separate Netlify/Vercel API functions need their own wrapper:
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
import { withStopnetic } from '@stopnetic/js/server';
|
|
226
|
+
const handler = withStopnetic(async (request, context) => {
|
|
227
|
+
// Also enforce your application's login/data permissions.
|
|
228
|
+
return Response.json({ example: true });
|
|
229
|
+
});
|
|
230
|
+
// Netlify: export default handler;
|
|
231
|
+
// export const config = { path: '/api/private', preferStatic: false };
|
|
232
|
+
// Vercel: export default { fetch: handler };
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Use the public custom `/api/private` path in all clients and Policies, not
|
|
236
|
+
Netlify's default `/.netlify/functions/` alias. Fetch same-origin data with
|
|
237
|
+
`credentials: 'same-origin'`. API requests cannot run a verification page;
|
|
238
|
+
verify on an HTML page first. An API on another origin needs its own server
|
|
239
|
+
protection and session, or a guarded same-origin server proxy. CORS alone does
|
|
240
|
+
not authorize data, and the current SDK does not share sessions across hosts.
|
|
241
|
+
|
|
242
|
+
## 6. Rules, deployment and testing
|
|
243
|
+
|
|
244
|
+
Save the actual page as Page / Automatic browser flow and its APIs as API /
|
|
245
|
+
Require verified session in Policies. First matching rule wins. Unmatched paths
|
|
246
|
+
are public in the generated site's whole-site mode. Public and block rules still
|
|
247
|
+
require a valid signed policy response. Verification can be completed by capable
|
|
248
|
+
automated browsers; retain your own authentication and authorization.
|
|
249
|
+
|
|
250
|
+
Run `stopnetic-js check`, build and deploy. Configure server env in each hosted
|
|
251
|
+
context and redeploy after changing it. For local Netlify/Vercel functions use
|
|
252
|
+
the full local server, not just `ng serve` or Vite. For Node use
|
|
253
|
+
`node --env-file=.env.local your-server.mjs` or your server environment loader.
|
|
254
|
+
|
|
255
|
+
Test raw document requests, verified browser requests, direct private API calls,
|
|
256
|
+
denial and service failure. Check status/body, session cookies, JS/CSS MIME types,
|
|
257
|
+
client navigation, entry HTML, aliases, prerender outputs and caches. A denied
|
|
258
|
+
request must not execute the renderer or data handler. The offline CLI check
|
|
259
|
+
validates configuration and generated files, not deployed coverage.
|
|
260
|
+
|
|
261
|
+
Ordinary policy changes apply on subsequent guarded requests without reinstalling
|
|
262
|
+
or redeploying. Already loaded content is not recalled. Public HTML/JS/assets and
|
|
263
|
+
content bundled into frontend code remain public; keep private data in guarded
|
|
264
|
+
server endpoints.
|
|
265
|
+
|
|
266
|
+
## Updating an existing generated guide
|
|
267
|
+
|
|
268
|
+
After installing the new SDK, rerun `stopnetic-js init --platform <same-platform>`
|
|
269
|
+
(preview with `--dry-run`). An unchanged 0.0.2-pilot generated guide is upgraded
|
|
270
|
+
with its setup manifest. Your key, cookie secret and server files are preserved.
|
|
271
|
+
Customized guides or handlers require manual merging; switching platforms also
|
|
272
|
+
requires review. This command does not publish, deploy or contact Stopnetic.
|
|
273
|
+
|
|
274
|
+
References: https://angular.dev/guide/ssr,
|
|
275
|
+
https://angular.dev/guide/routing/route-guards,
|
|
276
|
+
https://docs.netlify.com/build/frameworks/framework-setup-guides/angular/.
|
package/dist/server.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export function optionsFromEnv(env?: Record<string, string | undefined>): Stopne
|
|
|
5
5
|
export function createStopneticAccessHandler(options?: StopneticServerOptions): (request: Request) => Promise<Response>;
|
|
6
6
|
export function withStopnetic<Context extends unknown[]>(handler: (request: Request, ...context: Context) => Response | Promise<Response>,
|
|
7
7
|
options?: StopneticServerOptions): (request: Request, ...context: Context) => Promise<Response>;
|
|
8
|
+
/** Node.js application wrapper: control endpoints, pages and APIs before rendering. */
|
|
9
|
+
export function createStopneticAppHandler<Context extends unknown[]>(handler: (request: Request, ...context: Context) => Response | Promise<Response>,
|
|
10
|
+
options?: StopneticServerOptions): (request: Request, ...context: Context) => Promise<Response>;
|
|
8
11
|
export function createStopneticAccessMiddleware(options?: StopneticServerOptions): (
|
|
9
12
|
request: IncomingMessage & { originalUrl?: string }, response: ServerResponse, next: () => unknown,
|
|
10
13
|
) => Promise<unknown>;
|
package/dist/server.js
CHANGED
|
@@ -71,6 +71,22 @@ export function withStopnetic(handler, options = optionsFromEnv()) {
|
|
|
71
71
|
};
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/** Guard a complete Fetch-style application before its renderer or API runs.
|
|
75
|
+
* The application must run in Node.js; accepting Request/Response alone does
|
|
76
|
+
* not make an Edge runtime compatible. Context arguments are preserved.
|
|
77
|
+
*/
|
|
78
|
+
export function createStopneticAppHandler(handler, options = optionsFromEnv()) {
|
|
79
|
+
if (typeof handler !== 'function') throw new TypeError('An application handler is required');
|
|
80
|
+
const settings = { ...options, verifyPath: `${accessBase}/verify` };
|
|
81
|
+
const access = createStopneticAccessHandler(settings);
|
|
82
|
+
const application = withStopnetic(handler, settings);
|
|
83
|
+
return (request, ...context) => {
|
|
84
|
+
const path = new URL(request.url).pathname;
|
|
85
|
+
if (path === accessBase || path.startsWith(`${accessBase}/`)) return access(request);
|
|
86
|
+
return application(request, ...context);
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
74
90
|
/** Node/Express: mount first, then the root guard, then parsers/static/API handlers. */
|
|
75
91
|
export function createStopneticAccessMiddleware(options = optionsFromEnv()) {
|
|
76
92
|
const access = createStopneticAccessHandler(options);
|
package/dist/setup.js
CHANGED
|
@@ -35,62 +35,9 @@ function platformFor(root, supplied) {
|
|
|
35
35
|
if (vercel && !netlify) return 'vercel';
|
|
36
36
|
throw new SetupError('Choose your hosting explicitly: --platform netlify, vercel or node.');
|
|
37
37
|
}
|
|
38
|
-
const
|
|
38
|
+
const legacyGuideDigest = 'b1f149af44f9ceb08fb80c18cb5d4b521620196f536d8ad422ab94d15d1d125d';
|
|
39
|
+
const clientGuide = readFileSync(new URL('./client-guide.md', import.meta.url), 'utf8');
|
|
39
40
|
|
|
40
|
-
Copy .env.stopnetic.example settings to the hosting platform's SERVER environment.
|
|
41
|
-
Use Node.js 22+. Never prefix private settings with VITE_ or REACT_APP_.
|
|
42
|
-
Use the same cookie secret locally and in hosting; do not commit .env.local.
|
|
43
|
-
|
|
44
|
-
## React Router (inside BrowserRouter)
|
|
45
|
-
|
|
46
|
-
import { useLocation } from 'react-router-dom';
|
|
47
|
-
import { StopneticBoundary } from '@stopnetic/js/react';
|
|
48
|
-
|
|
49
|
-
function RouteGuard({ children }) {
|
|
50
|
-
const location = useLocation();
|
|
51
|
-
return <StopneticBoundary path={location.pathname + location.search}>
|
|
52
|
-
{children}
|
|
53
|
-
</StopneticBoundary>;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
Wrap your Routes with RouteGuard. Other frontends can await
|
|
57
|
-
checkAccess(path) from @stopnetic/js/browser before rendering; when reason is
|
|
58
|
-
verification_required, navigate to result.verificationUrl. Handle denial/outages.
|
|
59
|
-
|
|
60
|
-
## API protection
|
|
61
|
-
|
|
62
|
-
EACH protected Netlify/Vercel Fetch-style API must use:
|
|
63
|
-
import { withStopnetic } from '@stopnetic/js/server';
|
|
64
|
-
const protectedHandler = withStopnetic(async (request, context) => {
|
|
65
|
-
// Enforce your application's login/data permissions here too.
|
|
66
|
-
return Response.json({ example: true });
|
|
67
|
-
});
|
|
68
|
-
// Netlify: export default protectedHandler;
|
|
69
|
-
// export const config = { path: '/api/private', preferStatic: false };
|
|
70
|
-
// Vercel: export default { fetch: protectedHandler };
|
|
71
|
-
|
|
72
|
-
Node/Express: call installStopnetic(app) from ./stopnetic.server.mjs BEFORE
|
|
73
|
-
parsers, static files and routes. Only expose that guarded server to visitors.
|
|
74
|
-
|
|
75
|
-
Add a Page / Automatic browser flow rule and API / Require verified session rules
|
|
76
|
-
in Stopnetic Policies. The generated setup leaves unmatched routes public.
|
|
77
|
-
Netlify's default /.netlify/functions/ aliases are rejected by the wrapper; use
|
|
78
|
-
custom config.path endpoints in React and Policies. Fetch protected data with
|
|
79
|
-
credentials: 'same-origin'. API requests cannot start
|
|
80
|
-
the browser verification flow. All control endpoints must reach their function
|
|
81
|
-
before a SPA catch-all; verify redirects and custom function directories.
|
|
82
|
-
|
|
83
|
-
Deploy once, then change rules in the dashboard. Rules apply to subsequent
|
|
84
|
-
guarded requests; loaded content is not recalled. Wrap new API handlers too.
|
|
85
|
-
For local Netlify/Vercel testing, use their full local development server
|
|
86
|
-
(netlify dev / vercel dev), not Vite alone: the server functions must run too.
|
|
87
|
-
For Node, load .env.local with node --env-file=.env.local your-server.mjs,
|
|
88
|
-
or your existing server environment loader.
|
|
89
|
-
Static HTML/JS and content bundled into the frontend remain public. A UI gate
|
|
90
|
-
does not secure that content. Put private data in guarded server endpoints.
|
|
91
|
-
No Edge/Deno/Cloudflare Workers adapter is supplied. Next.js uses @stopnetic/next.
|
|
92
|
-
Test real hosting, custom domains, cookies, caches, aliases and API enforcement.
|
|
93
|
-
`;
|
|
94
41
|
function filesFor(root, platform) {
|
|
95
42
|
const files = {
|
|
96
43
|
'stopnetic-client-guide.md': clientGuide,
|
|
@@ -121,15 +68,21 @@ export function setup(input, { platform: supplied, dryRun = false } = {}) {
|
|
|
121
68
|
const platform = platformFor(root, supplied);
|
|
122
69
|
const files = filesFor(root, platform);
|
|
123
70
|
const manifestPath = destination(root, manifestName);
|
|
71
|
+
let manifest;
|
|
124
72
|
if (existsSync(manifestPath)) {
|
|
125
|
-
let manifest;
|
|
126
73
|
try { manifest = JSON.parse(text(manifestPath)); } catch { throw new SetupError('Check the existing Stopnetic setup manifest.'); }
|
|
127
74
|
if (manifest.platform !== platform) throw new SetupError('A different hosting setup already exists. Review it before changing platform.');
|
|
128
75
|
}
|
|
129
|
-
|
|
76
|
+
const upgraded = new Set();
|
|
77
|
+
// Validate every destination before making any change. Only an unchanged
|
|
78
|
+
// older generated guide can be replaced; customer modifications are refused.
|
|
130
79
|
for (const [name, content] of Object.entries(files)) {
|
|
131
80
|
const file = destination(root, name);
|
|
132
|
-
if (existsSync(file) && text(file) !== content)
|
|
81
|
+
if (existsSync(file) && text(file) !== content) {
|
|
82
|
+
if (name === 'stopnetic-client-guide.md' && manifest?.files?.[name] === legacyGuideDigest
|
|
83
|
+
&& digest(text(file)) === legacyGuideDigest) upgraded.add(name);
|
|
84
|
+
else throw new SetupError(`Existing file needs manual merging: ${name}`);
|
|
85
|
+
}
|
|
133
86
|
}
|
|
134
87
|
const local = destination(root, '.env.local');
|
|
135
88
|
const ignore = destination(root, '.gitignore');
|
|
@@ -138,7 +91,8 @@ export function setup(input, { platform: supplied, dryRun = false } = {}) {
|
|
|
138
91
|
for (const [name, content] of Object.entries(files)) {
|
|
139
92
|
const file = destination(root, name);
|
|
140
93
|
mkdirSync(dirname(file), { recursive: true });
|
|
141
|
-
if (
|
|
94
|
+
if (upgraded.has(name)) writeFileSync(file, content);
|
|
95
|
+
else if (!existsSync(file)) writeFileSync(file, content, { flag: 'wx' });
|
|
142
96
|
}
|
|
143
97
|
if (!env.STOPNETIC_COOKIE_SECRET) writeFileSync(local, `${text(local).replace(/\n?$/, '\n')}STOPNETIC_COOKIE_SECRET=${randomBytes(32).toString('base64url')}\n`, { mode: 0o600 });
|
|
144
98
|
if (!/(^|\n)\.env\.local$/.test(text(ignore).trimEnd())) writeFileSync(ignore, `${text(ignore).replace(/\n?$/, '\n')}.env.local\n`);
|