@stopnetic/js 0.0.1-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/LICENSE +21 -21
- package/README.md +372 -199
- package/bin/stopnetic-js.js +34 -0
- package/dist/browser.d.ts +4 -0
- package/dist/browser.js +21 -0
- package/dist/client-guide.md +276 -0
- package/dist/guard.d.ts +23 -21
- package/dist/guard.js +213 -200
- package/dist/index.d.ts +15 -15
- package/dist/index.js +172 -172
- package/dist/react.d.ts +4 -0
- package/dist/react.js +24 -0
- package/dist/server.d.ts +17 -0
- package/dist/server.js +123 -0
- package/dist/setup.js +119 -0
- package/dist/signing-keys.js +37 -26
- package/package.json +54 -47
package/README.md
CHANGED
|
@@ -1,199 +1,372 @@
|
|
|
1
|
-
# @stopnetic/js — JavaScript website guard
|
|
2
|
-
|
|
3
|
-
Stopnetic checks access **on your server before sending pages, files or API data**.
|
|
4
|
-
Your frontend can be Angular, Vue, React, Svelte, plain JavaScript, or an older
|
|
5
|
-
frontend build. No React/Vue/Angular dependency or version upgrade is required.
|
|
6
|
-
The hosting server must run Node.js 22 or newer. This is an MIT-licensed pilot SDK;
|
|
7
|
-
hosted production/provider validation is still required before paid rollout.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
the hosting
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
1
|
+
# @stopnetic/js — JavaScript website guard
|
|
2
|
+
|
|
3
|
+
Stopnetic checks access **on your server before sending pages, files or API data**.
|
|
4
|
+
Your frontend can be Angular, Vue, React, Svelte, plain JavaScript, or an older
|
|
5
|
+
frontend build. No React/Vue/Angular dependency or version upgrade is required.
|
|
6
|
+
The hosting server must run Node.js 22 or newer. This is an MIT-licensed pilot SDK;
|
|
7
|
+
hosted production/provider validation is still required before paid rollout.
|
|
8
|
+
|
|
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
|
|
18
|
+
and Node/Express, a browser access check and optional React boundary. It also
|
|
19
|
+
normalizes public PEM formatting and rejects placeholder or reused installation
|
|
20
|
+
keys as cookie secrets. Install the published pilot with:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm install @stopnetic/js@pilot
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
For local testing, build and pack from this repository:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
cd integrations/javascript
|
|
30
|
+
npm run build
|
|
31
|
+
npm pack
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Install the resulting `stopnetic-js-0.0.3-pilot.tgz` into the website project using
|
|
35
|
+
`npm install <absolute-path-to-tarball>`. The installed package has no Next.js or
|
|
36
|
+
other runtime dependency. The build packages the same signed decision implementation
|
|
37
|
+
as the Next.js adapter. Keep `@stopnetic/next` for its Next.js 16 guided installer.
|
|
38
|
+
|
|
39
|
+
## Reusable client/server setup (requires 0.0.2-pilot)
|
|
40
|
+
|
|
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.
|
|
48
|
+
|
|
49
|
+
Run setup at the app's package.json, choose hosting, and preview before writing:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
npm exec --no -- stopnetic-js init --platform netlify --dry-run
|
|
53
|
+
npm exec --no -- stopnetic-js init --platform netlify
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `--platform vercel` or `--platform node` for the other adapters. Netlify/Vercel
|
|
57
|
+
configuration files can select a platform automatically; ambiguous projects need
|
|
58
|
+
explicit selection. Next.js projects use `@stopnetic/next`. This SDK needs a
|
|
59
|
+
Node.js 22+ runtime; it does not run as a Deno/Workers/Edge function.
|
|
60
|
+
|
|
61
|
+
Setup prepares a server control endpoint, `.env.stopnetic.example`, an ignored
|
|
62
|
+
`.env.local` cookie secret, a setup manifest and `stopnetic-client-guide.md`.
|
|
63
|
+
It does not install dependencies, edit your React app, overwrite existing files,
|
|
64
|
+
contact providers or deploy. Changed generated files require manual merging.
|
|
65
|
+
Custom Netlify base directories require review; custom functions directories are
|
|
66
|
+
preserved. Vercel configuration is preserved. Put runtime settings in the hosting
|
|
67
|
+
provider's server/Functions environment; plain `netlify.toml` build env is not a
|
|
68
|
+
Functions secret store. Copy the generated cookie secret without committing it.
|
|
69
|
+
|
|
70
|
+
Wire browser route checks and EACH protected API following the generated guide.
|
|
71
|
+
Run `npm exec --no -- stopnetic-js check` after setting credentials (`--local-http`
|
|
72
|
+
for a loopback pilot). This check validates configuration and generated files
|
|
73
|
+
offline; it cannot prove deployment routing, API coverage or browser integration.
|
|
74
|
+
Deploy once, then dashboard changes apply on subsequent guarded requests.
|
|
75
|
+
|
|
76
|
+
Test locally with `netlify dev` or `vercel dev`, which must run both the frontend
|
|
77
|
+
and server functions; Vite alone does not provide the generated server endpoints.
|
|
78
|
+
For Node, use `node --env-file=.env.local your-server.mjs` or your existing
|
|
79
|
+
server environment loader. Production credentials belong in runtime settings.
|
|
80
|
+
|
|
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
|
|
106
|
+
|
|
107
|
+
The optional React entry uses Hooks. The core/browser modules need no React.
|
|
108
|
+
Place this component inside BrowserRouter, wrapping your Routes:
|
|
109
|
+
|
|
110
|
+
```jsx
|
|
111
|
+
import { useLocation } from 'react-router-dom';
|
|
112
|
+
import { StopneticBoundary } from '@stopnetic/js/react';
|
|
113
|
+
|
|
114
|
+
function RouteGuard({ children }) {
|
|
115
|
+
const location = useLocation();
|
|
116
|
+
return <StopneticBoundary path={location.pathname + location.search}>
|
|
117
|
+
{children}
|
|
118
|
+
</StopneticBoundary>;
|
|
119
|
+
}
|
|
120
|
+
// <BrowserRouter><RouteGuard><Routes>...</Routes></RouteGuard></BrowserRouter>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The boundary hides children while checking, ignores canceled/stale checks and
|
|
124
|
+
redirects to same-origin verification when required. Block/outage displays a
|
|
125
|
+
denial; customize `pending` and `blocked` with your UI. Vue, Angular, Svelte and
|
|
126
|
+
plain JS can await `checkAccess(path)` from `@stopnetic/js/browser` and use
|
|
127
|
+
`result.allowed`, `result.reason` and `result.verificationUrl` in their router.
|
|
128
|
+
This is a UI check, never an authorization credential for a data request.
|
|
129
|
+
|
|
130
|
+
### Netlify / Vercel APIs
|
|
131
|
+
|
|
132
|
+
Import server helpers only in server files. Wrap every protected function before
|
|
133
|
+
it loads/returns data, keeping your application's own login/data permissions:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
import { withStopnetic } from '@stopnetic/js/server';
|
|
137
|
+
const api = withStopnetic(async request => {
|
|
138
|
+
// Enforce application permissions, then retrieve private data here.
|
|
139
|
+
return Response.json({ example: true });
|
|
140
|
+
});
|
|
141
|
+
// Netlify function:
|
|
142
|
+
export default api;
|
|
143
|
+
export const config = { path: '/api/private', preferStatic: false };
|
|
144
|
+
// Vercel api/private.mjs instead: export default { fetch: api };
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Netlify's default `/.netlify/functions/` aliases are rejected by this wrapper
|
|
148
|
+
so they cannot bypass rules on a custom path. Use custom `config.path` endpoints
|
|
149
|
+
in your frontend and Policies. Keep the generated `/api/stopnetic/access` and
|
|
150
|
+
`/api/stopnetic/verify` endpoints reachable before any SPA catch-all. The access
|
|
151
|
+
endpoint checks the requested page using signed decisions, establishes browser
|
|
152
|
+
verification and redirects back to the same origin. It never returns private
|
|
153
|
+
content or an API authorization token. API wrappers independently check every
|
|
154
|
+
request and preserve ordinary request bodies. Denials/outages return empty
|
|
155
|
+
404/503 responses; approved data responses use `private, no-store`.
|
|
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
|
+
|
|
172
|
+
### Node servers on other hosts
|
|
173
|
+
|
|
174
|
+
Import `installStopnetic` from generated `stopnetic.server.mjs`, then call
|
|
175
|
+
`installStopnetic(app)` before Express parsers, static files, SPA fallback and
|
|
176
|
+
API handlers. This checks the control endpoints and the rest of the server.
|
|
177
|
+
It can run on any host supporting Node.js 22+, not just Netlify/Vercel. For a
|
|
178
|
+
custom Fetch-based Node framework, use the shared `createStopneticAccessHandler`
|
|
179
|
+
and `withStopnetic` in its request hooks. Existing low-level adapters remain
|
|
180
|
+
available below with their original default-blocking behavior.
|
|
181
|
+
|
|
182
|
+
### Rules and the protection boundary
|
|
183
|
+
|
|
184
|
+
The generated setup opts into dashboard-managed mode: unmatched guarded requests
|
|
185
|
+
are public. Add explicit Page / Automatic browser flow rules and API / Require
|
|
186
|
+
verified session rules. Keep private data exclusively in guarded APIs. An API
|
|
187
|
+
fetch does not start browser verification; it needs the established same-origin
|
|
188
|
+
cookie. Static HTML/JavaScript, routes without a browser boundary, external APIs,
|
|
189
|
+
unwrapped handlers, alternate hosts and previously loaded content are not made
|
|
190
|
+
private by this setup. No installer can hide content already in a public bundle.
|
|
191
|
+
Browser automation can still complete verification. Test your actual provider,
|
|
192
|
+
custom-domain routing, redirects, cookies, caches and direct API access before
|
|
193
|
+
rollout. Hosting contracts are simulated locally; live provider tests are pending.
|
|
194
|
+
|
|
195
|
+
## Which installation fits my site?
|
|
196
|
+
|
|
197
|
+
| Site | Where to install |
|
|
198
|
+
| --- | --- |
|
|
199
|
+
| Angular, Vue, React, Svelte SPA or plain HTML/JS | Node/Express server serving its built output, before static files and SPA fallback |
|
|
200
|
+
| Existing Node/Express server, including Express 4 and 5 | First root middleware, before body parsers, routes and data handlers |
|
|
201
|
+
| Node-hosted framework with Web `Request`/`Response` hooks | `createStopneticHandler` before the framework renders or serves data |
|
|
202
|
+
| Next.js 16 App Router | Published `@stopnetic/next@pilot` and its guided installer |
|
|
203
|
+
| Older Next.js with a customer-owned Node server | Generic middleware before the Next request handler; native older Next installers are not supplied |
|
|
204
|
+
| Static-only hosting / browser-only scripts | Needs a server or compatible edge/proxy in front, or a separately protected content API |
|
|
205
|
+
|
|
206
|
+
Do not import this SDK in browser components or add a public `<script>` containing
|
|
207
|
+
your installation key. Hiding a screen cannot protect HTML, JS bundles or data that
|
|
208
|
+
the hosting provider already serves publicly. This package uses `node:crypto`; it
|
|
209
|
+
is not a Cloudflare Workers/browser/Edge-runtime adapter. Framework-native SSR
|
|
210
|
+
adapters and serverless platform wiring need their own deployment validation.
|
|
211
|
+
|
|
212
|
+
## Server configuration
|
|
213
|
+
|
|
214
|
+
Add a site in Stopnetic, verify its domain, and create an installation key in
|
|
215
|
+
Integration. Site ID identifies your website; the private installation key
|
|
216
|
+
authenticates it. Copy the service public signing key from Integration too.
|
|
217
|
+
Set these **server-only** environment variables, using your actual URLs and keys:
|
|
218
|
+
|
|
219
|
+
```dotenv
|
|
220
|
+
STOPNETIC_WEBSITE_ORIGIN=https://www.example.com
|
|
221
|
+
STOPNETIC_SITE_ID=<generated-site-id>
|
|
222
|
+
STOPNETIC_API_URL=https://<your-stopnetic-service>/api/integration/decision
|
|
223
|
+
STOPNETIC_API_TOKEN=<your-one-time-installation-key>
|
|
224
|
+
STOPNETIC_DECISION_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n<service-public-key>\n-----END PUBLIC KEY-----\n"
|
|
225
|
+
STOPNETIC_COOKIE_SECRET=<separate-random-secret-at-least-32-characters>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Generate the cookie secret with
|
|
229
|
+
`node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"`.
|
|
230
|
+
Never use frontend prefixes such as `VITE_`, `NEXT_PUBLIC_` or `REACT_APP_` for secrets.
|
|
231
|
+
The optional `STOPNETIC_DECISION_ADDITIONAL_PUBLIC_KEYS` is a JSON array of at most
|
|
232
|
+
two explicitly trusted public PEMs for staged signing-key rotation.
|
|
233
|
+
|
|
234
|
+
The origin is the public website's protocol and hostname, with an optional port.
|
|
235
|
+
No `/home`, credentials, query or fragment. HTTPS is required except on localhost.
|
|
236
|
+
Configure your TLS proxy to preserve the public `Host` header and keep the backend
|
|
237
|
+
private. The adapter uses the configured origin and never trusts forwarded host or
|
|
238
|
+
protocol headers. HTTPS origins always produce secure cookies; HTTP localhost
|
|
239
|
+
origins automatically use development cookies.
|
|
240
|
+
|
|
241
|
+
## Node/Express middleware
|
|
242
|
+
|
|
243
|
+
```js
|
|
244
|
+
import express from 'express';
|
|
245
|
+
import { createStopneticMiddleware, parseAdditionalDecisionPublicKeys } from '@stopnetic/js';
|
|
246
|
+
|
|
247
|
+
const app = express();
|
|
248
|
+
app.set('case sensitive routing', true);
|
|
249
|
+
app.set('strict routing', true);
|
|
250
|
+
app.use(createStopneticMiddleware({
|
|
251
|
+
origin: process.env.STOPNETIC_WEBSITE_ORIGIN,
|
|
252
|
+
siteId: process.env.STOPNETIC_SITE_ID,
|
|
253
|
+
apiUrl: process.env.STOPNETIC_API_URL,
|
|
254
|
+
apiToken: process.env.STOPNETIC_API_TOKEN,
|
|
255
|
+
decisionPublicKey: process.env.STOPNETIC_DECISION_PUBLIC_KEY?.replaceAll('\\n', '\n'),
|
|
256
|
+
additionalDecisionPublicKeys: parseAdditionalDecisionPublicKeys(process.env.STOPNETIC_DECISION_ADDITIONAL_PUBLIC_KEYS),
|
|
257
|
+
cookieSecret: process.env.STOPNETIC_COOKIE_SECRET,
|
|
258
|
+
}));
|
|
259
|
+
// Your existing application follows the guard.
|
|
260
|
+
app.use(express.json());
|
|
261
|
+
app.post('/api/private', yourExistingApiHandler);
|
|
262
|
+
app.use(express.static('dist', { dotfiles: 'deny', cacheControl: false }));
|
|
263
|
+
// Add your existing guarded SPA fallback here.
|
|
264
|
+
app.listen(8080);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Install `express` separately in your website if needed. The repository includes
|
|
268
|
+
`integrations/javascript/examples/express-server.mjs` for a complete SPA server.
|
|
269
|
+
Serve only the build output directory (`dist`, `build`, or Angular's browser output),
|
|
270
|
+
never your project root. A framework-independent fallback after your static middleware:
|
|
271
|
+
|
|
272
|
+
```js
|
|
273
|
+
app.use((req, res) => {
|
|
274
|
+
if (req.method === 'GET' && req.get('accept')?.includes('text/html')
|
|
275
|
+
&& !req.path.startsWith('/api/') && !req.path.split('/').some(p => p.startsWith('.'))) {
|
|
276
|
+
res.sendFile('index.html', { root: absoluteBuildDirectory, cacheControl: false });
|
|
277
|
+
} else res.status(404).end();
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Place the guard **first at the root**, before body parsers and before anything can
|
|
282
|
+
send bytes. It checks every request except explicitly configured `publicPaths`.
|
|
283
|
+
Ordinary POST bodies stay intact for your application's parser. The same middleware
|
|
284
|
+
handles `/__stopnetic/verify`; do not mount it only on `/members`.
|
|
285
|
+
|
|
286
|
+
With Node's built-in HTTP server, the same middleware works without Express:
|
|
287
|
+
|
|
288
|
+
```js
|
|
289
|
+
import { createServer } from 'node:http';
|
|
290
|
+
const protect = createStopneticMiddleware(options);
|
|
291
|
+
createServer((req, res) => {
|
|
292
|
+
void protect(req, res, () => existingApplication(req, res)).catch(() => res.destroy());
|
|
293
|
+
}).listen(8080);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
For an older CommonJS application, initialize at startup with
|
|
297
|
+
`const { createStopneticMiddleware } = await import('@stopnetic/js')` inside an async
|
|
298
|
+
function. Only the server runtime needs to meet the minimum version; the frontend
|
|
299
|
+
framework can remain unchanged.
|
|
300
|
+
|
|
301
|
+
## Request/Response hook
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
import { createStopneticHandler } from '@stopnetic/js';
|
|
305
|
+
const protect = createStopneticHandler(options); // same options, including origin
|
|
306
|
+
|
|
307
|
+
export async function handle({ request, resolve }) {
|
|
308
|
+
return protect(request, checkedRequest => resolve(checkedRequest));
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Wire this function to the Node-hosted framework's actual request hook. For SvelteKit,
|
|
313
|
+
the hook receives an `event`, so use
|
|
314
|
+
`protect(event.request, () => resolve(event))` in `hooks.server.js`. The request URL
|
|
315
|
+
must contain the configured public origin. Configure the framework's trusted proxy
|
|
316
|
+
settings explicitly. This is a generic hook example, not a tested matrix of all
|
|
317
|
+
SvelteKit/Nuxt/framework releases.
|
|
318
|
+
|
|
319
|
+
## Policies, failures and hosting
|
|
320
|
+
|
|
321
|
+
In Policies, choose public, automatic browser flow, verified session, or blocked
|
|
322
|
+
routes. Automatic flow serves a short verification page first, then a cookie-bound
|
|
323
|
+
session; requests from ordinary fetchers and non-navigation clients get no protected
|
|
324
|
+
bytes. Browser-capable automation may complete the challenge. Your application must
|
|
325
|
+
still enforce its own sign-in, membership and purchase permissions. Entitlement
|
|
326
|
+
rules are not implemented and withhold content.
|
|
327
|
+
|
|
328
|
+
Both adapters automatically route verification, bound its body to 4 KiB and five
|
|
329
|
+
seconds, and reject foreign origins. Protected requests fail closed with empty
|
|
330
|
+
404/503 responses on denial, malformed signed decisions or service outages. Approved
|
|
331
|
+
application responses receive `Cache-Control: private, no-store`; middleware users
|
|
332
|
+
must keep that header in their downstream handlers. Prevent CDN, prerendered files,
|
|
333
|
+
service workers or another origin from serving protected content before the guard.
|
|
334
|
+
WebSockets/upgrades are not covered; protect them separately or disable them.
|
|
335
|
+
|
|
336
|
+
By default **all paths are checked**, including assets and APIs. Optional local
|
|
337
|
+
`publicPaths: ['/health', '/public/*']` bypass the policy service even during outages.
|
|
338
|
+
Use only paths deliberately intended to remain public. Ambiguous encoded separators,
|
|
339
|
+
dot segments, encoded ASCII characters, repeated slashes and malformed targets are
|
|
340
|
+
rejected by the Node middleware. Policy paths are case sensitive. Match your router's
|
|
341
|
+
case/slash behavior to the policies, cover directory-index and other aliases, and
|
|
342
|
+
keep the service's default blocking behavior. Avoid a broad public catch-all that
|
|
343
|
+
could expose an alternative path to protected content.
|
|
344
|
+
|
|
345
|
+
An installed guard's first valid request starts the site's account trial. Key
|
|
346
|
+
rotation/revocation and domain/account/subscription gates are checked by the service.
|
|
347
|
+
Access reports are best effort and cannot establish delivery guarantees.
|
|
348
|
+
With the updated dashboard, prepared 0.0.2 links same-page challenges, verification
|
|
349
|
+
and the first reported access result. The Challenge outcomes panel distinguishes
|
|
350
|
+
allowed, blocked, waiting, not completed and verified without an access result;
|
|
351
|
+
older evidence may be unavailable. It does not identify humans or bots. Install
|
|
352
|
+
the updated SDK on your server after deploying the dashboard update.
|
|
353
|
+
|
|
354
|
+
Validate your actual hosting, cache rules, browser flow, API paths and provider configuration
|
|
355
|
+
before onboarding paying customers.
|
|
356
|
+
|
|
357
|
+
## Maintainer checks and release
|
|
358
|
+
|
|
359
|
+
From this folder: `npm ci`, `npm test`, `npm run test:package`. Tests include real
|
|
360
|
+
Node HTTP traffic, Express 4/5 static/SPA/API serving, challenges, body preservation,
|
|
361
|
+
signed decision failures and package consumption from ESM, CommonJS and TypeScript.
|
|
362
|
+
The tarball includes only `dist`, README, MIT license and manifest. No secrets,
|
|
363
|
+
examples, tests, build scripts or dashboard are published. Build from the repository
|
|
364
|
+
before packing; published consumers do not need the repository or build scripts.
|
|
365
|
+
|
|
366
|
+
Publishing is a separate owner step after these checks:
|
|
367
|
+
|
|
368
|
+
```sh
|
|
369
|
+
npm publish --access public --tag pilot --registry=https://registry.npmjs.org/
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Publishing this package does not update the existing `@stopnetic/next` release.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { setup, checkSetup, SetupError } from '../dist/setup.js';
|
|
3
|
+
const help = `Stopnetic JavaScript setup — Node.js 22+
|
|
4
|
+
stopnetic-js init --platform netlify|vercel|node [--dry-run]
|
|
5
|
+
stopnetic-js check [--local-http]
|
|
6
|
+
|
|
7
|
+
Netlify/Vercel config files can select the platform automatically. Explicit
|
|
8
|
+
selection is otherwise required. Setup creates a server control endpoint,
|
|
9
|
+
browser/API integration guide and a private cookie secret, preserving customer
|
|
10
|
+
files. Follow the guide to wire route checks and EACH protected API. The package
|
|
11
|
+
does not make bundled frontend content private. Server settings stay out of
|
|
12
|
+
browser code. No network calls, package installation or deployment occurs.
|
|
13
|
+
`;
|
|
14
|
+
try {
|
|
15
|
+
const [operation, ...args] = process.argv.slice(2);
|
|
16
|
+
if (!operation || ['help', '-h', '--help'].includes(operation)) process.stdout.write(help);
|
|
17
|
+
else {
|
|
18
|
+
if (!['init', 'check'].includes(operation)) throw new SetupError('Choose init or check. Use --help for usage.');
|
|
19
|
+
const options = {};
|
|
20
|
+
for (let index = 0; index < args.length; index++) {
|
|
21
|
+
const flag = args[index];
|
|
22
|
+
if (flag === '--platform' && operation === 'init' && !options.platform) {
|
|
23
|
+
options.platform = args[++index];
|
|
24
|
+
if (!options.platform) throw new SetupError('Choose netlify, vercel or node after --platform.');
|
|
25
|
+
} else if (flag === '--dry-run' && operation === 'init') options.dryRun = true;
|
|
26
|
+
else if (flag === '--local-http' && operation === 'check') options.localHttp = true;
|
|
27
|
+
else throw new SetupError('Unsupported option. Use --help for usage.');
|
|
28
|
+
}
|
|
29
|
+
process.stdout.write((operation === 'init' ? setup(process.cwd(), options) : checkSetup(process.cwd(), options)) + '\n');
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
process.stderr.write((error instanceof SetupError ? error.message : 'Setup failed. Check project files and permissions.') + '\n');
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type AccessResult = { allowed: true } | {
|
|
2
|
+
allowed: false; reason: 'blocked' | 'unavailable' | 'verification_required'; verificationUrl?: string;
|
|
3
|
+
};
|
|
4
|
+
export function checkAccess(path: string, options?: { signal?: AbortSignal; fetchImpl?: typeof fetch }): Promise<AccessResult>;
|