@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.
@@ -0,0 +1,21 @@
1
+ // Browser-only entry: no Node imports, installation keys or cookie secrets.
2
+ export async function checkAccess(path, { signal, fetchImpl = globalThis.fetch } = {}) {
3
+ if (typeof path !== 'string' || !path.startsWith('/') || path.startsWith('//') || /[\\\s#]/.test(path)) throw new Error('Use a same-origin page path');
4
+ const url = `/api/stopnetic/access?${new URLSearchParams({ path, mode: 'check' })}`;
5
+ const controller = new AbortController();
6
+ const abort = () => controller.abort();
7
+ if (signal?.aborted) abort();
8
+ signal?.addEventListener('abort', abort, { once: true });
9
+ const timer = setTimeout(abort, 10000);
10
+ let response, result;
11
+ try {
12
+ response = await fetchImpl(url, { credentials: 'same-origin', cache: 'no-store', signal: controller.signal, headers: { Accept: 'application/json' } });
13
+ result = await response.json();
14
+ } catch (error) { if (signal?.aborted) throw error; return { allowed: false, reason: 'unavailable' }; }
15
+ finally { clearTimeout(timer); signal?.removeEventListener('abort', abort); }
16
+ if (response.status === 200 && result?.allowed === true) return { allowed: true };
17
+ if (response.status === 404 && result?.reason === 'verification_required') {
18
+ return { allowed: false, reason: 'verification_required', verificationUrl: `/api/stopnetic/access?${new URLSearchParams({ path })}` };
19
+ }
20
+ return { allowed: false, reason: response.status === 404 ? 'blocked' : 'unavailable' };
21
+ }
@@ -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/guard.d.ts CHANGED
@@ -1,21 +1,23 @@
1
- export type StopneticOptions = {
2
- siteId: string;
3
- apiUrl: string;
4
- apiToken: string;
5
- decisionPublicKey: string;
6
- /** Explicitly trusted replacement/retiring public PEMs, at most two. */
7
- additionalDecisionPublicKeys?: string[];
8
- cookieSecret: string;
9
- verifyPath?: string;
10
- cookieName?: string;
11
- secureCookie?: boolean;
12
- fetchImpl?: typeof fetch;
13
- timeoutMs?: number;
14
- publicPaths?: string[];
15
- reportEvents?: boolean;
16
- };
17
- export function parseAdditionalDecisionPublicKeys(value?: string): string[];
18
- export function createStopnetic(options: StopneticOptions): {
19
- check(request: Request): Promise<Response | null>;
20
- verify(request: Request): Promise<Response>;
21
- };
1
+ export type StopneticOptions = {
2
+ siteId: string;
3
+ apiUrl: string;
4
+ apiToken: string;
5
+ decisionPublicKey: string;
6
+ /** Explicitly trusted replacement/retiring public PEMs, at most two. */
7
+ additionalDecisionPublicKeys?: string[];
8
+ cookieSecret: string;
9
+ verifyPath?: string;
10
+ cookieName?: string;
11
+ secureCookie?: boolean;
12
+ fetchImpl?: typeof fetch;
13
+ timeoutMs?: number;
14
+ publicPaths?: string[];
15
+ reportEvents?: boolean;
16
+ /** Whole-site mode: unmatched routes public; requires the updated service. */
17
+ guardMode?: 'selected' | 'site';
18
+ };
19
+ export function parseAdditionalDecisionPublicKeys(value?: string): string[];
20
+ export function createStopnetic(options: StopneticOptions): {
21
+ check(request: Request): Promise<Response | null>;
22
+ verify(request: Request): Promise<Response>;
23
+ };