@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/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
- This package is prepared locally and **not published yet**. After publication:
10
-
11
- ```sh
12
- npm install @stopnetic/js@pilot
13
- ```
14
-
15
- For local testing, build and pack from this repository:
16
-
17
- ```sh
18
- cd integrations/javascript
19
- npm run build
20
- npm pack
21
- ```
22
-
23
- Install the resulting `stopnetic-js-0.0.1-pilot.tgz` into the website project using
24
- `npm install <absolute-path-to-tarball>`. The installed package has no Next.js or
25
- other runtime dependency. The build packages the same signed decision implementation
26
- as the Next.js adapter. Keep `@stopnetic/next` for its Next.js 16 guided installer.
27
-
28
- ## Which installation fits my site?
29
-
30
- | Site | Where to install |
31
- | --- | --- |
32
- | Angular, Vue, React, Svelte SPA or plain HTML/JS | Node/Express server serving its built output, before static files and SPA fallback |
33
- | Existing Node/Express server, including Express 4 and 5 | First root middleware, before body parsers, routes and data handlers |
34
- | Node-hosted framework with Web `Request`/`Response` hooks | `createStopneticHandler` before the framework renders or serves data |
35
- | Next.js 16 App Router | Published `@stopnetic/next@pilot` and its guided installer |
36
- | Older Next.js with a customer-owned Node server | Generic middleware before the Next request handler; native older Next installers are not supplied |
37
- | Static-only hosting / browser-only scripts | Needs a server or compatible edge/proxy in front, or a separately protected content API |
38
-
39
- Do not import this SDK in browser components or add a public `<script>` containing
40
- your installation key. Hiding a screen cannot protect HTML, JS bundles or data that
41
- the hosting provider already serves publicly. This package uses `node:crypto`; it
42
- is not a Cloudflare Workers/browser/Edge-runtime adapter. Framework-native SSR
43
- adapters and serverless platform wiring need their own deployment validation.
44
-
45
- ## Server configuration
46
-
47
- Add a site in Stopnetic, verify its domain, and create an installation key in
48
- Integration. Site ID identifies your website; the private installation key
49
- authenticates it. Copy the service public signing key from Integration too.
50
- Set these **server-only** environment variables, using your actual URLs and keys:
51
-
52
- ```dotenv
53
- STOPNETIC_WEBSITE_ORIGIN=https://www.example.com
54
- STOPNETIC_SITE_ID=<generated-site-id>
55
- STOPNETIC_API_URL=https://<your-stopnetic-service>/api/integration/decision
56
- STOPNETIC_API_TOKEN=<your-one-time-installation-key>
57
- STOPNETIC_DECISION_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\n<service-public-key>\n-----END PUBLIC KEY-----\n"
58
- STOPNETIC_COOKIE_SECRET=<separate-random-secret-at-least-32-characters>
59
- ```
60
-
61
- Generate the cookie secret with
62
- `node -e "console.log(require('node:crypto').randomBytes(32).toString('base64url'))"`.
63
- Never use frontend prefixes such as `VITE_`, `NEXT_PUBLIC_` or `REACT_APP_` for secrets.
64
- The optional `STOPNETIC_DECISION_ADDITIONAL_PUBLIC_KEYS` is a JSON array of at most
65
- two explicitly trusted public PEMs for staged signing-key rotation.
66
-
67
- The origin is the public website's protocol and hostname, with an optional port.
68
- No `/home`, credentials, query or fragment. HTTPS is required except on localhost.
69
- Configure your TLS proxy to preserve the public `Host` header and keep the backend
70
- private. The adapter uses the configured origin and never trusts forwarded host or
71
- protocol headers. HTTPS origins always produce secure cookies; HTTP localhost
72
- origins automatically use development cookies.
73
-
74
- ## Node/Express middleware
75
-
76
- ```js
77
- import express from 'express';
78
- import { createStopneticMiddleware, parseAdditionalDecisionPublicKeys } from '@stopnetic/js';
79
-
80
- const app = express();
81
- app.set('case sensitive routing', true);
82
- app.set('strict routing', true);
83
- app.use(createStopneticMiddleware({
84
- origin: process.env.STOPNETIC_WEBSITE_ORIGIN,
85
- siteId: process.env.STOPNETIC_SITE_ID,
86
- apiUrl: process.env.STOPNETIC_API_URL,
87
- apiToken: process.env.STOPNETIC_API_TOKEN,
88
- decisionPublicKey: process.env.STOPNETIC_DECISION_PUBLIC_KEY?.replaceAll('\\n', '\n'),
89
- additionalDecisionPublicKeys: parseAdditionalDecisionPublicKeys(process.env.STOPNETIC_DECISION_ADDITIONAL_PUBLIC_KEYS),
90
- cookieSecret: process.env.STOPNETIC_COOKIE_SECRET,
91
- }));
92
- // Your existing application follows the guard.
93
- app.use(express.json());
94
- app.post('/api/private', yourExistingApiHandler);
95
- app.use(express.static('dist', { dotfiles: 'deny', cacheControl: false }));
96
- // Add your existing guarded SPA fallback here.
97
- app.listen(8080);
98
- ```
99
-
100
- Install `express` separately in your website if needed. The repository includes
101
- `integrations/javascript/examples/express-server.mjs` for a complete SPA server.
102
- Serve only the build output directory (`dist`, `build`, or Angular's browser output),
103
- never your project root. A framework-independent fallback after your static middleware:
104
-
105
- ```js
106
- app.use((req, res) => {
107
- if (req.method === 'GET' && req.get('accept')?.includes('text/html')
108
- && !req.path.startsWith('/api/') && !req.path.split('/').some(p => p.startsWith('.'))) {
109
- res.sendFile('index.html', { root: absoluteBuildDirectory, cacheControl: false });
110
- } else res.status(404).end();
111
- });
112
- ```
113
-
114
- Place the guard **first at the root**, before body parsers and before anything can
115
- send bytes. It checks every request except explicitly configured `publicPaths`.
116
- Ordinary POST bodies stay intact for your application's parser. The same middleware
117
- handles `/__stopnetic/verify`; do not mount it only on `/members`.
118
-
119
- With Node's built-in HTTP server, the same middleware works without Express:
120
-
121
- ```js
122
- import { createServer } from 'node:http';
123
- const protect = createStopneticMiddleware(options);
124
- createServer((req, res) => {
125
- void protect(req, res, () => existingApplication(req, res)).catch(() => res.destroy());
126
- }).listen(8080);
127
- ```
128
-
129
- For an older CommonJS application, initialize at startup with
130
- `const { createStopneticMiddleware } = await import('@stopnetic/js')` inside an async
131
- function. Only the server runtime needs to meet the minimum version; the frontend
132
- framework can remain unchanged.
133
-
134
- ## Request/Response hook
135
-
136
- ```js
137
- import { createStopneticHandler } from '@stopnetic/js';
138
- const protect = createStopneticHandler(options); // same options, including origin
139
-
140
- export async function handle({ request, resolve }) {
141
- return protect(request, checkedRequest => resolve(checkedRequest));
142
- }
143
- ```
144
-
145
- Wire this function to the Node-hosted framework's actual request hook. For SvelteKit,
146
- the hook receives an `event`, so use
147
- `protect(event.request, () => resolve(event))` in `hooks.server.js`. The request URL
148
- must contain the configured public origin. Configure the framework's trusted proxy
149
- settings explicitly. This is a generic hook example, not a tested matrix of all
150
- SvelteKit/Nuxt/framework releases.
151
-
152
- ## Policies, failures and hosting
153
-
154
- In Policies, choose public, automatic browser flow, verified session, or blocked
155
- routes. Automatic flow serves a short verification page first, then a cookie-bound
156
- session; requests from ordinary fetchers and non-navigation clients get no protected
157
- bytes. Browser-capable automation may complete the challenge. Your application must
158
- still enforce its own sign-in, membership and purchase permissions. Entitlement
159
- rules are not implemented and withhold content.
160
-
161
- Both adapters automatically route verification, bound its body to 4 KiB and five
162
- seconds, and reject foreign origins. Protected requests fail closed with empty
163
- 404/503 responses on denial, malformed signed decisions or service outages. Approved
164
- application responses receive `Cache-Control: private, no-store`; middleware users
165
- must keep that header in their downstream handlers. Prevent CDN, prerendered files,
166
- service workers or another origin from serving protected content before the guard.
167
- WebSockets/upgrades are not covered; protect them separately or disable them.
168
-
169
- By default **all paths are checked**, including assets and APIs. Optional local
170
- `publicPaths: ['/health', '/public/*']` bypass the policy service even during outages.
171
- Use only paths deliberately intended to remain public. Ambiguous encoded separators,
172
- dot segments, encoded ASCII characters, repeated slashes and malformed targets are
173
- rejected by the Node middleware. Policy paths are case sensitive. Match your router's
174
- case/slash behavior to the policies, cover directory-index and other aliases, and
175
- keep the service's default blocking behavior. Avoid a broad public catch-all that
176
- could expose an alternative path to protected content.
177
-
178
- An installed guard's first valid request starts the site's account trial. Key
179
- rotation/revocation and domain/account/subscription gates are checked by the service.
180
- Access reports are best effort and cannot establish delivery guarantees. Validate
181
- your actual hosting, cache rules, browser flow, API paths and provider configuration
182
- before onboarding paying customers.
183
-
184
- ## Maintainer checks and release
185
-
186
- From this folder: `npm ci`, `npm test`, `npm run test:package`. Tests include real
187
- Node HTTP traffic, Express 4/5 static/SPA/API serving, challenges, body preservation,
188
- signed decision failures and package consumption from ESM, CommonJS and TypeScript.
189
- The tarball includes only `dist`, README, MIT license and manifest. No secrets,
190
- examples, tests, build scripts or dashboard are published. Build from the repository
191
- before packing; published consumers do not need the repository or build scripts.
192
-
193
- Publishing is a separate owner step after these checks:
194
-
195
- ```sh
196
- npm publish --access public --tag pilot --registry=https://registry.npmjs.org/
197
- ```
198
-
199
- Publishing this package does not update the existing `@stopnetic/next` release.
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>;