@stopnetic/js 0.0.5-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 +77 -44
- package/dist/client-guide.md +406 -27
- package/dist/server.d.ts +17 -17
- package/dist/server.js +125 -123
- package/dist/setup.js +126 -124
- 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 -0
- package/examples/ssr/angular/server.ts +12 -0
- package/examples/ssr/astro/middleware.js +11 -0
- package/examples/ssr/astro/page.astro +4 -0
- package/examples/ssr/node/fetch.mjs +9 -0
- package/examples/ssr/node/install.mjs +9 -0
- package/examples/ssr/nuxt/middleware.mjs +10 -0
- package/examples/ssr/react/install.mjs +10 -0
- package/examples/ssr/react-router/install.mjs +22 -0
- package/examples/ssr/solidstart/middleware-v1.js +19 -0
- package/examples/ssr/solidstart/middleware-v2.js +25 -0
- package/examples/ssr/sveltekit/hooks.server.js +16 -0
- package/examples/ssr/sveltekit/page.server.js +3 -0
- package/examples/ssr/vue/install.mjs +10 -0
- package/package.json +37 -10
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { RouterLink, RouterView } from 'vue-router';
|
|
3
|
-
import { accessState } from './router.js';
|
|
4
|
-
</script>
|
|
5
|
-
|
|
6
|
-
<template>
|
|
7
|
-
<template v-if="accessState === 'allowed'">
|
|
8
|
-
<header><nav>
|
|
9
|
-
<RouterLink to="/">Home</RouterLink> <RouterLink to="/members">Members</RouterLink>
|
|
10
|
-
<RouterLink to="/slow">Slow</RouterLink> <RouterLink to="/blocked">Blocked</RouterLink>
|
|
11
|
-
<RouterLink to="/offline">Offline</RouterLink>
|
|
12
|
-
</nav></header>
|
|
13
|
-
<RouterView/>
|
|
14
|
-
<footer>Example site</footer>
|
|
15
|
-
</template>
|
|
16
|
-
<p v-else-if="accessState === 'denied'" role="alert">Access unavailable.</p>
|
|
17
|
-
</template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { RouterLink, RouterView } from 'vue-router';
|
|
3
|
+
import { accessState } from './router.js';
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<template v-if="accessState === 'allowed'">
|
|
8
|
+
<header><nav>
|
|
9
|
+
<RouterLink to="/">Home</RouterLink> <RouterLink to="/members">Members</RouterLink>
|
|
10
|
+
<RouterLink to="/slow">Slow</RouterLink> <RouterLink to="/blocked">Blocked</RouterLink>
|
|
11
|
+
<RouterLink to="/offline">Offline</RouterLink>
|
|
12
|
+
</nav></header>
|
|
13
|
+
<RouterView/>
|
|
14
|
+
<footer>Example site</footer>
|
|
15
|
+
</template>
|
|
16
|
+
<p v-else-if="accessState === 'denied'" role="alert">Access unavailable.</p>
|
|
17
|
+
</template>
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
import { onMounted, onUnmounted, ref } from 'vue';
|
|
3
|
-
import { loadPrivateData } from '../load-private-data.js';
|
|
4
|
-
const data = ref('Loading data...');
|
|
5
|
-
const controller = new AbortController();
|
|
6
|
-
onMounted(() => {
|
|
7
|
-
loadPrivateData(controller.signal)
|
|
8
|
-
.then(value => { if (!controller.signal.aborted) data.value = JSON.stringify(value); })
|
|
9
|
-
.catch(() => { if (!controller.signal.aborted) data.value = 'API access unavailable.'; });
|
|
10
|
-
});
|
|
11
|
-
onUnmounted(() => controller.abort());
|
|
12
|
-
</script>
|
|
13
|
-
|
|
14
|
-
<template><main><h1>Members</h1><p>{{ data }}</p></main></template>
|
|
1
|
+
<script setup>
|
|
2
|
+
import { onMounted, onUnmounted, ref } from 'vue';
|
|
3
|
+
import { loadPrivateData } from '../load-private-data.js';
|
|
4
|
+
const data = ref('Loading data...');
|
|
5
|
+
const controller = new AbortController();
|
|
6
|
+
onMounted(() => {
|
|
7
|
+
loadPrivateData(controller.signal)
|
|
8
|
+
.then(value => { if (!controller.signal.aborted) data.value = JSON.stringify(value); })
|
|
9
|
+
.catch(() => { if (!controller.signal.aborted) data.value = 'API access unavailable.'; });
|
|
10
|
+
});
|
|
11
|
+
onUnmounted(() => controller.abort());
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template><main><h1>Members</h1><p>{{ data }}</p></main></template>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createApp } from 'vue';
|
|
2
|
-
import App from './App.vue';
|
|
3
|
-
import { router } from './router.js';
|
|
4
|
-
createApp(App).use(router).mount('#app');
|
|
1
|
+
import { createApp } from 'vue';
|
|
2
|
+
import App from './App.vue';
|
|
3
|
+
import { router } from './router.js';
|
|
4
|
+
createApp(App).use(router).mount('#app');
|
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
import { h, ref } from 'vue';
|
|
2
|
-
import { createRouter, createWebHistory } from 'vue-router';
|
|
3
|
-
import { checkAccess } from '@stopnetic/js/browser';
|
|
4
|
-
import Members from './Members.vue';
|
|
5
|
-
|
|
6
|
-
export const accessState = ref('checking');
|
|
7
|
-
const page = title => ({ render: () => h('main', h('h1', title)) });
|
|
8
|
-
export const router = createRouter({
|
|
9
|
-
history: createWebHistory(),
|
|
10
|
-
routes: [
|
|
11
|
-
{ path: '/', component: page('Home') },
|
|
12
|
-
{ path: '/members', component: Members },
|
|
13
|
-
{ path: '/slow', component: page('Slow') },
|
|
14
|
-
{ path: '/blocked', component: page('Blocked') },
|
|
15
|
-
{ path: '/offline', component: page('Offline') },
|
|
16
|
-
],
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
let current;
|
|
20
|
-
let approvedPath = '';
|
|
21
|
-
router.beforeEach(async to => {
|
|
22
|
-
current?.abort();
|
|
23
|
-
const controller = new AbortController();
|
|
24
|
-
current = controller;
|
|
25
|
-
approvedPath = '';
|
|
26
|
-
accessState.value = 'checking';
|
|
27
|
-
try {
|
|
28
|
-
const result = await checkAccess(to.fullPath.split('#')[0], { signal: controller.signal });
|
|
29
|
-
if (controller.signal.aborted) return false;
|
|
30
|
-
if (result.allowed) { approvedPath = to.fullPath; return true; }
|
|
31
|
-
if (result.reason === 'verification_required' && result.verificationUrl) {
|
|
32
|
-
location.assign(result.verificationUrl);
|
|
33
|
-
}
|
|
34
|
-
accessState.value = 'denied';
|
|
35
|
-
} catch { if (!controller.signal.aborted) accessState.value = 'denied'; }
|
|
36
|
-
return false;
|
|
37
|
-
});
|
|
38
|
-
router.afterEach((to, _from, failure) => {
|
|
39
|
-
if (!failure && approvedPath === to.fullPath) accessState.value = 'allowed';
|
|
40
|
-
});
|
|
1
|
+
import { h, ref } from 'vue';
|
|
2
|
+
import { createRouter, createWebHistory } from 'vue-router';
|
|
3
|
+
import { checkAccess } from '@stopnetic/js/browser';
|
|
4
|
+
import Members from './Members.vue';
|
|
5
|
+
|
|
6
|
+
export const accessState = ref('checking');
|
|
7
|
+
const page = title => ({ render: () => h('main', h('h1', title)) });
|
|
8
|
+
export const router = createRouter({
|
|
9
|
+
history: createWebHistory(),
|
|
10
|
+
routes: [
|
|
11
|
+
{ path: '/', component: page('Home') },
|
|
12
|
+
{ path: '/members', component: Members },
|
|
13
|
+
{ path: '/slow', component: page('Slow') },
|
|
14
|
+
{ path: '/blocked', component: page('Blocked') },
|
|
15
|
+
{ path: '/offline', component: page('Offline') },
|
|
16
|
+
],
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
let current;
|
|
20
|
+
let approvedPath = '';
|
|
21
|
+
router.beforeEach(async to => {
|
|
22
|
+
current?.abort();
|
|
23
|
+
const controller = new AbortController();
|
|
24
|
+
current = controller;
|
|
25
|
+
approvedPath = '';
|
|
26
|
+
accessState.value = 'checking';
|
|
27
|
+
try {
|
|
28
|
+
const result = await checkAccess(to.fullPath.split('#')[0], { signal: controller.signal });
|
|
29
|
+
if (controller.signal.aborted) return false;
|
|
30
|
+
if (result.allowed) { approvedPath = to.fullPath; return true; }
|
|
31
|
+
if (result.reason === 'verification_required' && result.verificationUrl) {
|
|
32
|
+
location.assign(result.verificationUrl);
|
|
33
|
+
}
|
|
34
|
+
accessState.value = 'denied';
|
|
35
|
+
} catch { if (!controller.signal.aborted) accessState.value = 'denied'; }
|
|
36
|
+
return false;
|
|
37
|
+
});
|
|
38
|
+
router.afterEach((to, _from, failure) => {
|
|
39
|
+
if (!failure && approvedPath === to.fullPath) accessState.value = 'allowed';
|
|
40
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Merge into your existing Node SSR entry; retain host settings and context.
|
|
2
|
+
import { AngularAppEngine, createRequestHandler } from '@angular/ssr';
|
|
3
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
4
|
+
|
|
5
|
+
const engine = new AngularAppEngine();
|
|
6
|
+
let guarded: ReturnType<typeof createStopneticAppHandler> | undefined;
|
|
7
|
+
async function handle(request: Request): Promise<Response> {
|
|
8
|
+
guarded ??= createStopneticAppHandler(async checked =>
|
|
9
|
+
await engine.handle(checked) ?? new Response(null, { status: 404 }));
|
|
10
|
+
return guarded(request);
|
|
11
|
+
}
|
|
12
|
+
export const reqHandler = createRequestHandler(handle);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/middleware.js. Keep the existing Node deployment adapter.
|
|
2
|
+
import { defineMiddleware } from 'astro:middleware';
|
|
3
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
4
|
+
|
|
5
|
+
let guarded;
|
|
6
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
7
|
+
// Public prerender builds only; protected pages must export prerender=false.
|
|
8
|
+
if (context.isPrerendered) return next();
|
|
9
|
+
guarded ??= createStopneticAppHandler((_request, run) => run());
|
|
10
|
+
return guarded(context.request, () => next());
|
|
11
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Wrap your existing Node Request -> Response renderer; retain all context.
|
|
2
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
3
|
+
export function protectSSR(renderApplication) {
|
|
4
|
+
let guarded;
|
|
5
|
+
return (request, ...context) => {
|
|
6
|
+
guarded ??= createStopneticAppHandler(renderApplication);
|
|
7
|
+
return guarded(request, ...context);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Call before parsers, static files, APIs and your existing Node SSR renderer.
|
|
2
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
3
|
+
export function installStopneticSSR(app) {
|
|
4
|
+
let server;
|
|
5
|
+
app.use((req, res, next) => {
|
|
6
|
+
server ??= createStopneticServer();
|
|
7
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// server/middleware/00-stopnetic.mjs. Nuxt 3/4, Nitro 2, H3 1, NODE only.
|
|
2
|
+
// Use server middleware, not app/middleware or defineNuxtRouteMiddleware.
|
|
3
|
+
import { fromNodeMiddleware } from 'h3';
|
|
4
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
5
|
+
|
|
6
|
+
let server;
|
|
7
|
+
export default fromNodeMiddleware((req, res, next) => {
|
|
8
|
+
server ??= createStopneticServer();
|
|
9
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Existing custom React SSR server: call before assets, APIs and React rendering.
|
|
2
|
+
// Keep renderToPipeableStream/renderToReadableStream and hydration unchanged.
|
|
3
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
4
|
+
export function installStopneticReactSSR(app) {
|
|
5
|
+
let server;
|
|
6
|
+
app.use((req, res, next) => {
|
|
7
|
+
server ??= createStopneticServer();
|
|
8
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Call installStopneticReactRouter(app) before parsers, assets and the existing
|
|
2
|
+
// @react-router/express createRequestHandler. Keep that handler/context intact.
|
|
3
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
4
|
+
|
|
5
|
+
export function installStopneticReactRouter(app) {
|
|
6
|
+
let server;
|
|
7
|
+
app.use((req, res, next) => {
|
|
8
|
+
server ??= createStopneticServer();
|
|
9
|
+
return server.access(req, res, () => {
|
|
10
|
+
// React Router 7 single-fetch data URLs must use the page's policy.
|
|
11
|
+
// The renderer still receives the original request and unread body.
|
|
12
|
+
const url = new URL(req.originalUrl || req.url, 'https://example.invalid');
|
|
13
|
+
let checked = req;
|
|
14
|
+
if (url.pathname.endsWith('.data')) {
|
|
15
|
+
url.pathname = url.pathname === '/_root.data' ? '/' : url.pathname.slice(0, -5);
|
|
16
|
+
checked = Object.create(req);
|
|
17
|
+
checked.originalUrl = url.pathname + url.search;
|
|
18
|
+
}
|
|
19
|
+
return server.guard(checked, res, next);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// SolidStart 1: register using defineConfig({ middleware: './src/middleware.js' }).
|
|
2
|
+
// Keep the existing entry-server.tsx renderer. This is NOT SolidStart 2 config.
|
|
3
|
+
import { createMiddleware } from '@solidjs/start/middleware';
|
|
4
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
5
|
+
|
|
6
|
+
let check;
|
|
7
|
+
export default createMiddleware({
|
|
8
|
+
onRequest: async event => {
|
|
9
|
+
check ??= createStopneticAppHandler((_request, state) => {
|
|
10
|
+
state.allowed = true;
|
|
11
|
+
return new Response(null, { status: 204 });
|
|
12
|
+
});
|
|
13
|
+
const state = { allowed: false };
|
|
14
|
+
const response = await check(event.request, state);
|
|
15
|
+
if (!state.allowed) return response;
|
|
16
|
+
event.response.headers.set('cache-control', 'private, no-store');
|
|
17
|
+
// undefined continues to SolidStart; preserve its rendering and API handling.
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// SolidStart 2: register this module using solidStart({ middleware: ... }).
|
|
2
|
+
import { createMiddleware } from '@solidjs/start/middleware';
|
|
3
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
4
|
+
|
|
5
|
+
let check;
|
|
6
|
+
export default createMiddleware([
|
|
7
|
+
async (event, next) => {
|
|
8
|
+
check ??= createStopneticAppHandler((_request, state) => {
|
|
9
|
+
state.allowed = true;
|
|
10
|
+
return new Response(null, { status: 204 });
|
|
11
|
+
});
|
|
12
|
+
// Per-request state: a verification 204 must NEVER be mistaken for permission.
|
|
13
|
+
const state = { allowed: false };
|
|
14
|
+
const response = await check(event.req, state);
|
|
15
|
+
if (!state.allowed) return response;
|
|
16
|
+
event.res.headers.set('cache-control', 'private, no-store');
|
|
17
|
+
const result = await next(); // Preserve H3/SolidStart context and raw results.
|
|
18
|
+
if (!(result instanceof Response)) return result;
|
|
19
|
+
const headers = new Headers(result.headers);
|
|
20
|
+
headers.set('cache-control', 'private, no-store');
|
|
21
|
+
return new Response(result.body, {
|
|
22
|
+
status: result.status, statusText: result.statusText, headers,
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { building } from '$app/environment';
|
|
2
|
+
import { createStopneticAppHandler } from '@stopnetic/js/server';
|
|
3
|
+
|
|
4
|
+
let guarded;
|
|
5
|
+
// When merging existing hooks, run this first and put them in the continuation.
|
|
6
|
+
export async function handle({ event, resolve }) {
|
|
7
|
+
// Build-time public prerendering only. Protected routes must have prerender=false.
|
|
8
|
+
if (building) return resolve(event);
|
|
9
|
+
guarded ??= createStopneticAppHandler((_request, run) => run());
|
|
10
|
+
// SvelteKit exposes the canonical page URL on data requests. Check that page
|
|
11
|
+
// rather than allowing an unmatched /members/__data.json alias through.
|
|
12
|
+
const checked = event.isDataRequest
|
|
13
|
+
? new Request(event.url, { method: event.request.method, headers: event.request.headers })
|
|
14
|
+
: event.request;
|
|
15
|
+
return guarded(checked, () => resolve(event));
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Existing custom Vue SSR server: call before assets, APIs and Vue rendering.
|
|
2
|
+
// Keep createSSRApp/renderToString and client hydration unchanged.
|
|
3
|
+
import { createStopneticServer } from '@stopnetic/js/server';
|
|
4
|
+
export function installStopneticVueSSR(app) {
|
|
5
|
+
let server;
|
|
6
|
+
app.use((req, res, next) => {
|
|
7
|
+
server ??= createStopneticServer();
|
|
8
|
+
return server.access(req, res, () => server.guard(req, res, next));
|
|
9
|
+
});
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,45 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stopnetic/js",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Server-side Stopnetic guard for JavaScript websites, Node.js and Express-compatible servers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Stopnetic",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
|
-
"url": "git+https://github.com/KlimentSidorov/
|
|
10
|
+
"url": "git+https://github.com/KlimentSidorov/stopnetic.git",
|
|
11
11
|
"directory": "integrations/javascript"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"stopnetic",
|
|
15
15
|
"javascript",
|
|
16
|
+
"typescript",
|
|
16
17
|
"nodejs",
|
|
17
18
|
"express",
|
|
19
|
+
"react",
|
|
20
|
+
"angular",
|
|
21
|
+
"vue",
|
|
22
|
+
"svelte",
|
|
18
23
|
"access-control"
|
|
19
24
|
],
|
|
20
25
|
"files": [
|
|
21
26
|
"dist",
|
|
22
27
|
"bin",
|
|
23
|
-
"examples/frontend"
|
|
28
|
+
"examples/frontend",
|
|
29
|
+
"examples/ssr"
|
|
24
30
|
],
|
|
25
|
-
"bin": {
|
|
31
|
+
"bin": {
|
|
32
|
+
"stopnetic-js": "./bin/stopnetic-js.js"
|
|
33
|
+
},
|
|
26
34
|
"exports": {
|
|
27
35
|
".": {
|
|
28
36
|
"types": "./dist/index.d.ts",
|
|
29
37
|
"default": "./dist/index.js"
|
|
30
38
|
},
|
|
31
|
-
"./server": {
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
"./server": {
|
|
40
|
+
"types": "./dist/server.d.ts",
|
|
41
|
+
"default": "./dist/server.js"
|
|
42
|
+
},
|
|
43
|
+
"./browser": {
|
|
44
|
+
"types": "./dist/browser.d.ts",
|
|
45
|
+
"default": "./dist/browser.js"
|
|
46
|
+
},
|
|
47
|
+
"./react": {
|
|
48
|
+
"types": "./dist/react.d.ts",
|
|
49
|
+
"default": "./dist/react.js"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"react": "*"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"react": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
34
59
|
},
|
|
35
|
-
"peerDependencies": { "react": "*" },
|
|
36
|
-
"peerDependenciesMeta": { "react": { "optional": true } },
|
|
37
60
|
"engines": {
|
|
38
61
|
"node": ">=22"
|
|
39
62
|
},
|
|
40
63
|
"publishConfig": {
|
|
41
64
|
"access": "public",
|
|
42
|
-
"tag": "
|
|
65
|
+
"tag": "latest",
|
|
43
66
|
"registry": "https://registry.npmjs.org/"
|
|
44
67
|
},
|
|
45
68
|
"scripts": {
|
|
@@ -51,5 +74,9 @@
|
|
|
51
74
|
"devDependencies": {
|
|
52
75
|
"express": "^4.22.3",
|
|
53
76
|
"express5": "npm:express@^5.2.1"
|
|
77
|
+
},
|
|
78
|
+
"homepage": "https://github.com/KlimentSidorov/stopnetic/tree/main/integrations/javascript#readme",
|
|
79
|
+
"bugs": {
|
|
80
|
+
"url": "https://github.com/KlimentSidorov/stopnetic/issues"
|
|
54
81
|
}
|
|
55
82
|
}
|