@stopnetic/js 0.0.6-pilot → 0.1.0
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 +48 -48
- package/dist/client-guide.md +6 -3
- package/dist/server.d.ts +17 -17
- package/dist/server.js +125 -123
- package/dist/setup.js +126 -125
- package/examples/frontend/angular/app/app.config.ts +8 -8
- package/examples/frontend/angular/app/app.routes.ts +20 -20
- package/examples/frontend/angular/app/app.ts +21 -21
- package/examples/frontend/angular/app/members.page.ts +21 -21
- package/examples/frontend/angular/app/stopnetic-access.service.ts +27 -27
- package/examples/frontend/angular/main.ts +4 -4
- package/examples/frontend/load-private-data.d.ts +1 -1
- package/examples/frontend/load-private-data.js +9 -9
- package/examples/frontend/react/App.tsx +48 -48
- package/examples/frontend/react/main.tsx +3 -3
- package/examples/frontend/solid/App.tsx +54 -54
- package/examples/frontend/solid/main.tsx +3 -3
- package/examples/frontend/svelte/App.svelte +56 -56
- package/examples/frontend/svelte/main.js +4 -4
- package/examples/frontend/vanilla/main.js +46 -46
- package/examples/frontend/vue/App.vue +17 -17
- package/examples/frontend/vue/Members.vue +14 -14
- package/examples/frontend/vue/main.js +4 -4
- package/examples/frontend/vue/router.js +40 -40
- package/examples/ssr/angular/app.routes.server.ts +5 -5
- package/examples/ssr/angular/server.ts +12 -12
- package/examples/ssr/astro/middleware.js +11 -11
- package/examples/ssr/astro/page.astro +4 -4
- package/examples/ssr/node/fetch.mjs +9 -9
- package/examples/ssr/node/install.mjs +9 -9
- package/examples/ssr/nuxt/middleware.mjs +10 -10
- package/examples/ssr/react/install.mjs +10 -10
- package/examples/ssr/react-router/install.mjs +22 -22
- package/examples/ssr/solidstart/middleware-v1.js +19 -19
- package/examples/ssr/solidstart/middleware-v2.js +25 -25
- package/examples/ssr/sveltekit/hooks.server.js +16 -16
- package/examples/ssr/sveltekit/page.server.js +3 -3
- package/examples/ssr/vue/install.mjs +10 -10
- package/package.json +35 -9
package/README.md
CHANGED
|
@@ -1,40 +1,35 @@
|
|
|
1
|
-
# @stopnetic/js
|
|
1
|
+
# @stopnetic/js - JavaScript and TypeScript website guard
|
|
2
2
|
|
|
3
3
|
Stopnetic checks access **on your server before sending pages, files or API data**.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
and custom Node servers. Examples identify the actual server entry, prerender
|
|
13
|
-
settings, deployed Node runtime and context/body preservation. Next.js uses its
|
|
14
|
-
dedicated adapter; other languages/Edge are explicitly outside this Node SDK.
|
|
15
|
-
Unchanged 0.0.2/0.0.3/0.0.4/0.0.5 guides migrate safely; custom edits require merging.
|
|
16
|
-
SSR and static rendering remain separate choices, with document navigation
|
|
17
|
-
default and no global checking screen. Optional browser demos remain available.
|
|
18
|
-
Angular, React, Vue, Svelte, Solid and plain JavaScript share the browser/server
|
|
19
|
-
contracts; no React dependency is required. See the
|
|
20
|
-
[complete generated guide](dist/client-guide.md) for SSR, static builds, frameworks,
|
|
21
|
-
client navigation, API protection and runtime restrictions.
|
|
22
|
-
|
|
23
|
-
The currently published **0.0.5-pilot** clarifies rendering/navigation and includes full frontend examples and the
|
|
24
|
-
framework-independent server/browser setup, complete Node application wrapper
|
|
25
|
-
and older-guide migration. Version 0.0.2 added the reusable browser/server setup for Netlify, Vercel
|
|
26
|
-
and Node/Express, a browser access check and optional React boundary. It also
|
|
27
|
-
normalizes public PEM formatting and rejects placeholder or reused installation
|
|
28
|
-
keys as cookie secrets. Install the published pilot with:
|
|
4
|
+
Use it with Node.js and Express, with frontend examples for Angular, React, Vue,
|
|
5
|
+
Svelte, Solid and plain JavaScript. No frontend framework is a required dependency.
|
|
6
|
+
Your hosting server must run **Node.js 22 or newer**; browser-only JavaScript does
|
|
7
|
+
not enforce protection on its own. Keep your existing SSR or static rendering.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Run inside your website project:
|
|
29
12
|
|
|
30
13
|
```sh
|
|
31
|
-
npm install @stopnetic/js
|
|
14
|
+
npm install @stopnetic/js
|
|
32
15
|
```
|
|
33
16
|
|
|
17
|
+
For Next.js 16 App Router, use [@stopnetic/next](https://www.npmjs.com/package/@stopnetic/next).
|
|
18
|
+
|
|
19
|
+
This release includes server/browser helpers, framework-specific SSR instructions,
|
|
20
|
+
complete optional frontend examples, Netlify/Vercel/Node setup commands and bounded
|
|
21
|
+
public-key parsing for raw, escaped or JSON-quoted public PEM values. Existing
|
|
22
|
+
unmodified generated guides can migrate safely; customized files require merging.
|
|
23
|
+
|
|
24
|
+
Read the [complete generated guide](https://unpkg.com/@stopnetic/js@0.1.0/dist/client-guide.md)
|
|
25
|
+
for SSR, static builds, client navigation, API protection and runtime restrictions.
|
|
26
|
+
Validate the actual deployed routing, caching and provider configuration before
|
|
27
|
+
production use. Browser verification does not prove human identity.
|
|
28
|
+
|
|
34
29
|
## Framework implementation examples
|
|
35
30
|
|
|
36
31
|
SSR apps use the **SSR framework** selector, not the optional browser-demo
|
|
37
|
-
selector. [Framework SSR instructions](dist/client-guide.md#ssr-framework-instructions)
|
|
32
|
+
selector. [Framework SSR instructions](https://unpkg.com/@stopnetic/js@0.1.0/dist/client-guide.md#ssr-framework-instructions)
|
|
38
33
|
and `examples/ssr` contain matching server code:
|
|
39
34
|
|
|
40
35
|
| SSR app | Server connection |
|
|
@@ -104,19 +99,19 @@ npm run build
|
|
|
104
99
|
npm pack
|
|
105
100
|
```
|
|
106
101
|
|
|
107
|
-
Install the resulting `stopnetic-js-0.0.
|
|
102
|
+
Install the resulting `stopnetic-js-0.1.0.tgz` into the website project using
|
|
108
103
|
`npm install <absolute-path-to-tarball>`. The installed package has no Next.js or
|
|
109
104
|
other runtime dependency. The build packages the same signed decision implementation
|
|
110
105
|
as the Next.js adapter. Keep `@stopnetic/next` for its Next.js 16 guided installer.
|
|
111
106
|
|
|
112
|
-
## Reusable client/server setup
|
|
107
|
+
## Reusable client/server setup
|
|
113
108
|
|
|
114
|
-
Follow the [hosting guide](
|
|
109
|
+
Follow the [hosting guide](https://github.com/KlimentSidorov/stopnetic/blob/main/docs/javascript-hosting-guide.md) for the full
|
|
115
110
|
Netlify, Vercel or Node procedure, server settings, page HTTP 404 enforcement,
|
|
116
111
|
frontend/API wiring and diagnosis. The dashboard's Integration and Documentation
|
|
117
112
|
pages offer the same provider choices and complete copyable page-guard examples.
|
|
118
113
|
|
|
119
|
-
|
|
114
|
+
These commands and subpath exports are included in this release.
|
|
120
115
|
Older 0.0.1-pilot installations must upgrade before using them.
|
|
121
116
|
|
|
122
117
|
Run setup at the app's package.json, choose hosting, and preview before writing:
|
|
@@ -143,7 +138,7 @@ Functions secret store. Copy the generated cookie secret without committing it.
|
|
|
143
138
|
Wire the server page guard and EACH protected API following the generated guide.
|
|
144
139
|
Browser route checks are optional for retained SPA navigation.
|
|
145
140
|
Run `npm exec --no -- stopnetic-js check` after setting credentials (`--local-http`
|
|
146
|
-
for a loopback
|
|
141
|
+
for a loopback local test). This check validates configuration and generated files
|
|
147
142
|
offline; it cannot prove deployment routing, API coverage or browser integration.
|
|
148
143
|
Deploy once, then dashboard changes apply on subsequent guarded requests.
|
|
149
144
|
|
|
@@ -154,7 +149,7 @@ server environment loader. Production credentials belong in runtime settings.
|
|
|
154
149
|
|
|
155
150
|
### Shared browser startup and Node SSR
|
|
156
151
|
|
|
157
|
-
The generated [framework-independent guide](dist/client-guide.md) defaults to
|
|
152
|
+
The generated [framework-independent guide](https://unpkg.com/@stopnetic/js@0.1.0/dist/client-guide.md) defaults to
|
|
158
153
|
server document navigation: leave browser startup/layout intact, use normal
|
|
159
154
|
links, and let the server apply dashboard policies. It also gives an OPTIONAL
|
|
160
155
|
browser startup gate and separate OPTIONAL SPA router demos. Do not combine
|
|
@@ -232,10 +227,10 @@ request and preserve ordinary request bodies. Denials/outages return empty
|
|
|
232
227
|
|
|
233
228
|
For a real HTTP 404 on a Netlify SPA page, use the optional page-serving function
|
|
234
229
|
in `examples/netlify-spa-pages.mjs` and follow
|
|
235
|
-
[`docs/netlify-spa-page-requests.md`](
|
|
230
|
+
[`docs/netlify-spa-page-requests.md`](https://github.com/KlimentSidorov/stopnetic/blob/main/docs/netlify-spa-page-requests.md).
|
|
236
231
|
It checks signed access before reading the bundled entry HTML. The browser
|
|
237
232
|
boundary alone cannot change the initial document's HTTP status. This example
|
|
238
|
-
uses the
|
|
233
|
+
uses the server helpers included in this release; the setup CLI does not install it
|
|
239
234
|
automatically. Keep static bundles public and private data in guarded APIs.
|
|
240
235
|
|
|
241
236
|
For Vercel page responses, the manual Node routing middleware in
|
|
@@ -275,7 +270,7 @@ rollout. Hosting contracts are simulated locally; live provider tests are pendin
|
|
|
275
270
|
| Angular, Vue, React, Svelte SPA or plain HTML/JS | Node/Express server serving its built output, before static files and SPA fallback |
|
|
276
271
|
| Existing Node/Express server, including Express 4 and 5 | First root middleware, before body parsers, routes and data handlers |
|
|
277
272
|
| Node-hosted framework with Web `Request`/`Response` hooks | `createStopneticHandler` before the framework renders or serves data |
|
|
278
|
-
| Next.js 16 App Router | Published `@stopnetic/next
|
|
273
|
+
| Next.js 16 App Router | Published `@stopnetic/next` and its guided installer |
|
|
279
274
|
| Older Next.js with a customer-owned Node server | Generic middleware before the Next request handler; native older Next installers are not supplied |
|
|
280
275
|
| Static-only hosting / browser-only scripts | Needs a server or compatible edge/proxy in front, or a separately protected content API |
|
|
281
276
|
|
|
@@ -420,14 +415,14 @@ could expose an alternative path to protected content.
|
|
|
420
415
|
|
|
421
416
|
An installed guard's first valid request starts the site's account trial. Key
|
|
422
417
|
rotation/revocation and domain/account/subscription gates are checked by the service.
|
|
423
|
-
Access reports are best effort and cannot establish delivery guarantees.
|
|
424
|
-
With the updated dashboard,
|
|
425
|
-
and the first reported access result. The Challenge outcomes panel distinguishes
|
|
426
|
-
allowed, blocked, waiting, not completed and verified without an access result;
|
|
427
|
-
older evidence may be unavailable. It does not identify humans or bots. Install
|
|
428
|
-
the updated SDK on your server after deploying the dashboard update.
|
|
429
|
-
|
|
430
|
-
Validate your actual hosting, cache rules, browser flow, API paths and provider configuration
|
|
418
|
+
Access reports are best effort and cannot establish delivery guarantees.
|
|
419
|
+
With the updated dashboard, the SDK links same-page challenges, verification
|
|
420
|
+
and the first reported access result. The Challenge outcomes panel distinguishes
|
|
421
|
+
allowed, blocked, waiting, not completed and verified without an access result;
|
|
422
|
+
older evidence may be unavailable. It does not identify humans or bots. Install
|
|
423
|
+
the updated SDK on your server after deploying the dashboard update.
|
|
424
|
+
|
|
425
|
+
Validate your actual hosting, cache rules, browser flow, API paths and provider configuration
|
|
431
426
|
before onboarding paying customers.
|
|
432
427
|
|
|
433
428
|
## Maintainer checks and release
|
|
@@ -435,14 +430,19 @@ before onboarding paying customers.
|
|
|
435
430
|
From this folder: `npm ci`, `npm test`, `npm run test:package`. Tests include real
|
|
436
431
|
Node HTTP traffic, Express 4/5 static/SPA/API serving, challenges, body preservation,
|
|
437
432
|
signed decision failures and package consumption from ESM, CommonJS and TypeScript.
|
|
438
|
-
The tarball includes
|
|
439
|
-
examples,
|
|
433
|
+
The tarball includes runtime/types in `dist`, the setup CLI in `bin`, the public
|
|
434
|
+
frontend/SSR examples, README, MIT license and manifest. No secrets, tests, build
|
|
435
|
+
scripts or dashboard are published. Build from the repository
|
|
440
436
|
before packing; published consumers do not need the repository or build scripts.
|
|
441
437
|
|
|
442
438
|
Publishing is a separate owner step after these checks:
|
|
443
439
|
|
|
444
440
|
```sh
|
|
445
|
-
npm publish --access public --tag
|
|
441
|
+
npm publish --access public --tag latest --registry=https://registry.npmjs.org/
|
|
446
442
|
```
|
|
447
443
|
|
|
448
444
|
Publishing this package does not update the existing `@stopnetic/next` release.
|
|
445
|
+
|
|
446
|
+
## Source and support
|
|
447
|
+
|
|
448
|
+
[Source repository](https://github.com/KlimentSidorov/stopnetic/tree/main/integrations/javascript) | [Report an issue](https://github.com/KlimentSidorov/stopnetic/issues)
|
package/dist/client-guide.md
CHANGED
|
@@ -101,8 +101,11 @@ STOPNETIC_COOKIE_SECRET=your_separate_random_cookie_secret
|
|
|
101
101
|
|
|
102
102
|
The installer creates a private cookie secret in `.env.local` when missing.
|
|
103
103
|
Reuse it across deployments. Do not commit `.env.local`. Copy complete PEM
|
|
104
|
-
header/body/footer with real newlines or literal `\n
|
|
105
|
-
|
|
104
|
+
header/body/footer with real newlines or literal `\n`. The dashboard's generated
|
|
105
|
+
local configuration uses an unquoted single-line key with literal `\n`; the SDK
|
|
106
|
+
restores its line breaks. Hosting fields take only the value, with no variable
|
|
107
|
+
name, equals sign or surrounding quotes. Manually written actual multiline PEMs
|
|
108
|
+
still need quotes in env files. Previously generated quoted env files remain valid.
|
|
106
109
|
Never put private settings in frontend environment files or prefix them with
|
|
107
110
|
`VITE_`, `REACT_APP_` or `NEXT_PUBLIC_`. Angular's `environment.ts` is browser
|
|
108
111
|
configuration, not a private server environment.
|
|
@@ -1308,7 +1311,7 @@ server endpoints.
|
|
|
1308
1311
|
## Updating an existing generated guide
|
|
1309
1312
|
|
|
1310
1313
|
After installing the new SDK, rerun `stopnetic-js init --platform <same-platform>`
|
|
1311
|
-
(preview with `--dry-run`). An unchanged 0.0.2/0.0.3/0.0.4/0.0.5 generated guide is upgraded
|
|
1314
|
+
(preview with `--dry-run`). An unchanged 0.0.2/0.0.3/0.0.4/0.0.5/0.0.6 generated guide is upgraded
|
|
1312
1315
|
with its setup manifest. Your key, cookie secret and server files are preserved.
|
|
1313
1316
|
Customized guides or handlers require manual merging; switching platforms also
|
|
1314
1317
|
requires review. This command does not publish, deploy or contact Stopnetic.
|
package/dist/server.d.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import type { StopneticServerOptions } from './index.js';
|
|
3
|
-
export const accessBase: '/api/stopnetic';
|
|
4
|
-
export function optionsFromEnv(env?: Record<string, string | undefined>): StopneticServerOptions;
|
|
5
|
-
export function createStopneticAccessHandler(options?: StopneticServerOptions): (request: Request) => Promise<Response>;
|
|
6
|
-
export function withStopnetic<Context extends unknown[]>(handler: (request: Request, ...context: Context) => Response | Promise<Response>,
|
|
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>;
|
|
11
|
-
export function createStopneticAccessMiddleware(options?: StopneticServerOptions): (
|
|
12
|
-
request: IncomingMessage & { originalUrl?: string }, response: ServerResponse, next: () => unknown,
|
|
13
|
-
) => Promise<unknown>;
|
|
14
|
-
export function createStopneticServer(options?: StopneticServerOptions): {
|
|
15
|
-
access: ReturnType<typeof createStopneticAccessMiddleware>;
|
|
16
|
-
guard: ReturnType<typeof import('./index.js').createStopneticMiddleware>;
|
|
17
|
-
};
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import type { StopneticServerOptions } from './index.js';
|
|
3
|
+
export const accessBase: '/api/stopnetic';
|
|
4
|
+
export function optionsFromEnv(env?: Record<string, string | undefined>): StopneticServerOptions;
|
|
5
|
+
export function createStopneticAccessHandler(options?: StopneticServerOptions): (request: Request) => Promise<Response>;
|
|
6
|
+
export function withStopnetic<Context extends unknown[]>(handler: (request: Request, ...context: Context) => Response | Promise<Response>,
|
|
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>;
|
|
11
|
+
export function createStopneticAccessMiddleware(options?: StopneticServerOptions): (
|
|
12
|
+
request: IncomingMessage & { originalUrl?: string }, response: ServerResponse, next: () => unknown,
|
|
13
|
+
) => Promise<unknown>;
|
|
14
|
+
export function createStopneticServer(options?: StopneticServerOptions): {
|
|
15
|
+
access: ReturnType<typeof createStopneticAccessMiddleware>;
|
|
16
|
+
guard: ReturnType<typeof import('./index.js').createStopneticMiddleware>;
|
|
17
|
+
};
|
package/dist/server.js
CHANGED
|
@@ -1,123 +1,125 @@
|
|
|
1
|
-
import { createStopneticHandler, createStopneticMiddleware, parseAdditionalDecisionPublicKeys } from './index.js';
|
|
2
|
-
|
|
3
|
-
export const accessBase = '/api/stopnetic';
|
|
4
|
-
|
|
5
|
-
/** Server environment only. Never import this module into browser code. */
|
|
6
|
-
export function optionsFromEnv(env = process.env) {
|
|
7
|
-
return {
|
|
8
|
-
origin: env.STOPNETIC_WEBSITE_ORIGIN,
|
|
9
|
-
siteId: env.STOPNETIC_SITE_ID,
|
|
10
|
-
apiUrl: env.STOPNETIC_API_URL,
|
|
11
|
-
apiToken: env.STOPNETIC_API_TOKEN,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (url.
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
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
|
-
const
|
|
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
|
-
response
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
1
|
+
import { createStopneticHandler, createStopneticMiddleware, parseAdditionalDecisionPublicKeys } from './index.js';
|
|
2
|
+
|
|
3
|
+
export const accessBase = '/api/stopnetic';
|
|
4
|
+
|
|
5
|
+
/** Server environment only. Never import this module into browser code. */
|
|
6
|
+
export function optionsFromEnv(env = process.env) {
|
|
7
|
+
return {
|
|
8
|
+
origin: env.STOPNETIC_WEBSITE_ORIGIN,
|
|
9
|
+
siteId: env.STOPNETIC_SITE_ID,
|
|
10
|
+
apiUrl: env.STOPNETIC_API_URL,
|
|
11
|
+
apiToken: env.STOPNETIC_API_TOKEN,
|
|
12
|
+
// The shared parser decodes quotes BEFORE escaped newlines. Decoding here
|
|
13
|
+
// would turn a valid JSON-quoted PEM into invalid JSON with raw newlines.
|
|
14
|
+
decisionPublicKey: env.STOPNETIC_DECISION_PUBLIC_KEY,
|
|
15
|
+
additionalDecisionPublicKeys: parseAdditionalDecisionPublicKeys(env.STOPNETIC_DECISION_ADDITIONAL_PUBLIC_KEYS),
|
|
16
|
+
cookieSecret: env.STOPNETIC_COOKIE_SECRET,
|
|
17
|
+
guardMode: 'site',
|
|
18
|
+
verifyPath: `${accessBase}/verify`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const empty = status => new Response(null, { status, headers: { 'cache-control': 'private, no-store' } });
|
|
23
|
+
function checkedPath(path, origin) {
|
|
24
|
+
if (typeof path !== 'string' || path.length > 2048 || !path.startsWith('/') || path.startsWith('//')
|
|
25
|
+
|| /[\\\s#]/.test(path)) throw new Error('Invalid page path');
|
|
26
|
+
const url = new URL(path, origin);
|
|
27
|
+
if (url.origin !== origin || url.pathname + url.search !== path
|
|
28
|
+
|| url.pathname === accessBase || url.pathname.startsWith(`${accessBase}/`)) throw new Error('Invalid page path');
|
|
29
|
+
return url;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Same-origin UI checks. These never authorize a separate data handler. */
|
|
33
|
+
export function createStopneticAccessHandler(options = optionsFromEnv()) {
|
|
34
|
+
const origin = new URL(options.origin).origin;
|
|
35
|
+
const verifyPath = `${accessBase}/verify`;
|
|
36
|
+
const protect = createStopneticHandler({ ...options, verifyPath });
|
|
37
|
+
return async function access(request) {
|
|
38
|
+
const url = new URL(request.url);
|
|
39
|
+
if (url.origin !== origin || (request.headers.has('host') && request.headers.get('host').toLowerCase() !== url.host)) return empty(404);
|
|
40
|
+
if (url.pathname === verifyPath) return protect(request, () => empty(404));
|
|
41
|
+
if (url.pathname !== `${accessBase}/access`) return empty(404);
|
|
42
|
+
if (request.method !== 'GET') return new Response(null, { status: 405, headers: { allow: 'GET', 'cache-control': 'private, no-store' } });
|
|
43
|
+
let target;
|
|
44
|
+
try { target = checkedPath(url.searchParams.get('path'), origin); } catch { return empty(400); }
|
|
45
|
+
if (url.searchParams.getAll('path').length !== 1 || url.searchParams.getAll('mode').length > 1
|
|
46
|
+
|| [...url.searchParams.keys()].some(key => !['path', 'mode'].includes(key))
|
|
47
|
+
|| (url.searchParams.has('mode') && url.searchParams.get('mode') !== 'check')) return empty(400);
|
|
48
|
+
// Navigation checks use the real page path and cookie, not the control URL.
|
|
49
|
+
const headers = new Headers(request.headers);
|
|
50
|
+
headers.set('accept', 'text/html');
|
|
51
|
+
const check = url.searchParams.get('mode') === 'check';
|
|
52
|
+
const response = await protect(new Request(target, { headers }), () => check
|
|
53
|
+
? Response.json({ allowed: true })
|
|
54
|
+
: new Response(null, { status: 303, headers: { location: target.pathname + target.search } }));
|
|
55
|
+
if (!check || response.ok) return response;
|
|
56
|
+
const challenge = response.status === 404 && response.headers.get('content-type')?.startsWith('text/html');
|
|
57
|
+
const resultHeaders = new Headers({ 'cache-control': 'private, no-store' });
|
|
58
|
+
for (const cookie of response.headers.getSetCookie()) resultHeaders.append('set-cookie', cookie);
|
|
59
|
+
await response.body?.cancel();
|
|
60
|
+
return Response.json({ allowed: false, reason: challenge ? 'verification_required' : response.status === 404 ? 'blocked' : 'unavailable' },
|
|
61
|
+
{ status: response.status, headers: resultHeaders });
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Wrap EACH protected Fetch-style API; the UI's allowed flag is never trusted. */
|
|
66
|
+
export function withStopnetic(handler, options = optionsFromEnv()) {
|
|
67
|
+
const protect = createStopneticHandler(options);
|
|
68
|
+
return (request, ...context) => {
|
|
69
|
+
// Netlify's default function alias must not bypass a custom API path's rule.
|
|
70
|
+
// Declare a custom config.path for these functions and use it in Policies.
|
|
71
|
+
if (new URL(request.url).pathname.startsWith('/.netlify/functions/')) return Promise.resolve(empty(404));
|
|
72
|
+
return protect(request, checked => handler(checked, ...context));
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Guard a complete Fetch-style application before its renderer or API runs.
|
|
77
|
+
* The application must run in Node.js; accepting Request/Response alone does
|
|
78
|
+
* not make an Edge runtime compatible. Context arguments are preserved.
|
|
79
|
+
*/
|
|
80
|
+
export function createStopneticAppHandler(handler, options = optionsFromEnv()) {
|
|
81
|
+
if (typeof handler !== 'function') throw new TypeError('An application handler is required');
|
|
82
|
+
const settings = { ...options, verifyPath: `${accessBase}/verify` };
|
|
83
|
+
const access = createStopneticAccessHandler(settings);
|
|
84
|
+
const application = withStopnetic(handler, settings);
|
|
85
|
+
return (request, ...context) => {
|
|
86
|
+
const path = new URL(request.url).pathname;
|
|
87
|
+
if (path === accessBase || path.startsWith(`${accessBase}/`)) return access(request);
|
|
88
|
+
return application(request, ...context);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Node/Express: mount first, then the root guard, then parsers/static/API handlers. */
|
|
93
|
+
export function createStopneticAccessMiddleware(options = optionsFromEnv()) {
|
|
94
|
+
const access = createStopneticAccessHandler(options);
|
|
95
|
+
const verification = createStopneticMiddleware({ ...options, verifyPath: `${accessBase}/verify` });
|
|
96
|
+
return async (req, res, next) => {
|
|
97
|
+
const path = (req.originalUrl || req.url || '').split('?')[0];
|
|
98
|
+
if (path !== `${accessBase}/access` && path !== `${accessBase}/verify`) return next();
|
|
99
|
+
if (path === `${accessBase}/verify`) return verification(req, res, next);
|
|
100
|
+
try {
|
|
101
|
+
const headers = new Headers();
|
|
102
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
103
|
+
if (Array.isArray(value)) { res.writeHead(400, { 'cache-control': 'no-store' }); res.end(); return; }
|
|
104
|
+
if (value !== undefined) headers.set(name, value);
|
|
105
|
+
}
|
|
106
|
+
const request = new Request(new URL(req.originalUrl || req.url, options.origin), {
|
|
107
|
+
method: req.method, headers,
|
|
108
|
+
...(['GET', 'HEAD'].includes(req.method) ? {} : { body: req, duplex: 'half' }),
|
|
109
|
+
});
|
|
110
|
+
const response = await access(request);
|
|
111
|
+
res.statusCode = response.status;
|
|
112
|
+
response.headers.forEach((value, name) => { if (name !== 'set-cookie') res.setHeader(name, value); });
|
|
113
|
+
const cookies = response.headers.getSetCookie();
|
|
114
|
+
if (cookies.length) res.setHeader('set-cookie', cookies);
|
|
115
|
+
res.end(Buffer.from(await response.arrayBuffer()));
|
|
116
|
+
} catch { res.writeHead(503, { 'cache-control': 'private, no-store' }); res.end(); }
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function createStopneticServer(options = optionsFromEnv()) {
|
|
121
|
+
return {
|
|
122
|
+
access: createStopneticAccessMiddleware(options),
|
|
123
|
+
guard: createStopneticMiddleware({ ...options, verifyPath: `${accessBase}/verify` }),
|
|
124
|
+
};
|
|
125
|
+
}
|