@stopnetic/js 0.0.6-pilot → 0.1.1

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.
Files changed (43) hide show
  1. package/README.md +75 -48
  2. package/dist/client-guide.md +6 -3
  3. package/dist/guard.d.ts +7 -0
  4. package/dist/guard.js +44 -20
  5. package/dist/index.js +7 -0
  6. package/dist/server.d.ts +17 -17
  7. package/dist/server.js +128 -123
  8. package/dist/setup.js +126 -125
  9. package/dist/traffic-context.js +26 -0
  10. package/examples/frontend/angular/app/app.config.ts +8 -8
  11. package/examples/frontend/angular/app/app.routes.ts +20 -20
  12. package/examples/frontend/angular/app/app.ts +21 -21
  13. package/examples/frontend/angular/app/members.page.ts +21 -21
  14. package/examples/frontend/angular/app/stopnetic-access.service.ts +27 -27
  15. package/examples/frontend/angular/main.ts +4 -4
  16. package/examples/frontend/load-private-data.d.ts +1 -1
  17. package/examples/frontend/load-private-data.js +9 -9
  18. package/examples/frontend/react/App.tsx +48 -48
  19. package/examples/frontend/react/main.tsx +3 -3
  20. package/examples/frontend/solid/App.tsx +54 -54
  21. package/examples/frontend/solid/main.tsx +3 -3
  22. package/examples/frontend/svelte/App.svelte +56 -56
  23. package/examples/frontend/svelte/main.js +4 -4
  24. package/examples/frontend/vanilla/main.js +46 -46
  25. package/examples/frontend/vue/App.vue +17 -17
  26. package/examples/frontend/vue/Members.vue +14 -14
  27. package/examples/frontend/vue/main.js +4 -4
  28. package/examples/frontend/vue/router.js +40 -40
  29. package/examples/ssr/angular/app.routes.server.ts +5 -5
  30. package/examples/ssr/angular/server.ts +12 -12
  31. package/examples/ssr/astro/middleware.js +11 -11
  32. package/examples/ssr/astro/page.astro +4 -4
  33. package/examples/ssr/node/fetch.mjs +9 -9
  34. package/examples/ssr/node/install.mjs +9 -9
  35. package/examples/ssr/nuxt/middleware.mjs +10 -10
  36. package/examples/ssr/react/install.mjs +10 -10
  37. package/examples/ssr/react-router/install.mjs +22 -22
  38. package/examples/ssr/solidstart/middleware-v1.js +19 -19
  39. package/examples/ssr/solidstart/middleware-v2.js +25 -25
  40. package/examples/ssr/sveltekit/hooks.server.js +16 -16
  41. package/examples/ssr/sveltekit/page.server.js +3 -3
  42. package/examples/ssr/vue/install.mjs +10 -10
  43. package/package.json +35 -9
@@ -1,20 +1,20 @@
1
- import { Component } from '@angular/core';
2
- import { Routes } from '@angular/router';
3
- import { MembersPage } from './members.page';
4
- import { stopneticRouteGuard } from './stopnetic-access.service';
5
-
6
- @Component({ template: '<main><h1>Home</h1></main>' })
7
- class HomePage {}
8
- @Component({ template: '<main><h1>Slow</h1></main>' })
9
- class OtherPage {}
10
-
11
- const pages: Routes = [
12
- { path: '', pathMatch: 'full', component: HomePage },
13
- { path: 'members', component: MembersPage },
14
- { path: 'slow', component: OtherPage },
15
- { path: 'blocked', component: OtherPage },
16
- { path: 'offline', component: OtherPage },
17
- ];
18
- export const routes: Routes = pages.map(route => ({
19
- ...route, canActivate: [stopneticRouteGuard], runGuardsAndResolvers: 'always' as const,
20
- }));
1
+ import { Component } from '@angular/core';
2
+ import { Routes } from '@angular/router';
3
+ import { MembersPage } from './members.page';
4
+ import { stopneticRouteGuard } from './stopnetic-access.service';
5
+
6
+ @Component({ template: '<main><h1>Home</h1></main>' })
7
+ class HomePage {}
8
+ @Component({ template: '<main><h1>Slow</h1></main>' })
9
+ class OtherPage {}
10
+
11
+ const pages: Routes = [
12
+ { path: '', pathMatch: 'full', component: HomePage },
13
+ { path: 'members', component: MembersPage },
14
+ { path: 'slow', component: OtherPage },
15
+ { path: 'blocked', component: OtherPage },
16
+ { path: 'offline', component: OtherPage },
17
+ ];
18
+ export const routes: Routes = pages.map(route => ({
19
+ ...route, canActivate: [stopneticRouteGuard], runGuardsAndResolvers: 'always' as const,
20
+ }));
@@ -1,21 +1,21 @@
1
- import { Component, inject } from '@angular/core';
2
- import { RouterLink, RouterOutlet } from '@angular/router';
3
- import { StopneticAccess } from './stopnetic-access.service';
4
-
5
- @Component({
6
- selector: 'app-root', imports: [RouterLink, RouterOutlet],
7
- template: `
8
- @if (access.state() === 'allowed') {
9
- <header><nav>
10
- <a routerLink="/">Home</a> <a routerLink="/members">Members</a>
11
- <a routerLink="/slow">Slow</a> <a routerLink="/blocked">Blocked</a>
12
- <a routerLink="/offline">Offline</a>
13
- </nav></header>
14
- <router-outlet/>
15
- <footer>Example site</footer>
16
- } @else if (access.state() === 'denied') {
17
- <p role="alert">Access unavailable.</p>
18
- }
19
- `,
20
- })
21
- export class App { readonly access = inject(StopneticAccess); }
1
+ import { Component, inject } from '@angular/core';
2
+ import { RouterLink, RouterOutlet } from '@angular/router';
3
+ import { StopneticAccess } from './stopnetic-access.service';
4
+
5
+ @Component({
6
+ selector: 'app-root', imports: [RouterLink, RouterOutlet],
7
+ template: `
8
+ @if (access.state() === 'allowed') {
9
+ <header><nav>
10
+ <a routerLink="/">Home</a> <a routerLink="/members">Members</a>
11
+ <a routerLink="/slow">Slow</a> <a routerLink="/blocked">Blocked</a>
12
+ <a routerLink="/offline">Offline</a>
13
+ </nav></header>
14
+ <router-outlet/>
15
+ <footer>Example site</footer>
16
+ } @else if (access.state() === 'denied') {
17
+ <p role="alert">Access unavailable.</p>
18
+ }
19
+ `,
20
+ })
21
+ export class App { readonly access = inject(StopneticAccess); }
@@ -1,21 +1,21 @@
1
- import { isPlatformBrowser } from '@angular/common';
2
- import { Component, DestroyRef, inject, OnInit, PLATFORM_ID, signal } from '@angular/core';
3
- import { HttpClient } from '@angular/common/http';
4
- import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
-
6
- @Component({ template: '<main><h1>Members</h1><p>{{ data() }}</p></main>' })
7
- export class MembersPage implements OnInit {
8
- readonly data = signal('Loading data...');
9
- private readonly http = inject(HttpClient);
10
- private readonly destroyRef = inject(DestroyRef);
11
- private readonly platform = inject(PLATFORM_ID);
12
- ngOnInit() {
13
- // This example loads data in the browser. SSR fetches need request cookies
14
- // forwarded explicitly, plus server authorization; do not bypass the guard.
15
- if (!isPlatformBrowser(this.platform)) return;
16
- this.http.get('/api/private', { withCredentials: true })
17
- .pipe(takeUntilDestroyed(this.destroyRef))
18
- .subscribe({ next: value => this.data.set(JSON.stringify(value)),
19
- error: () => this.data.set('API access unavailable.') });
20
- }
21
- }
1
+ import { isPlatformBrowser } from '@angular/common';
2
+ import { Component, DestroyRef, inject, OnInit, PLATFORM_ID, signal } from '@angular/core';
3
+ import { HttpClient } from '@angular/common/http';
4
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
+
6
+ @Component({ template: '<main><h1>Members</h1><p>{{ data() }}</p></main>' })
7
+ export class MembersPage implements OnInit {
8
+ readonly data = signal('Loading data...');
9
+ private readonly http = inject(HttpClient);
10
+ private readonly destroyRef = inject(DestroyRef);
11
+ private readonly platform = inject(PLATFORM_ID);
12
+ ngOnInit() {
13
+ // This example loads data in the browser. SSR fetches need request cookies
14
+ // forwarded explicitly, plus server authorization; do not bypass the guard.
15
+ if (!isPlatformBrowser(this.platform)) return;
16
+ this.http.get('/api/private', { withCredentials: true })
17
+ .pipe(takeUntilDestroyed(this.destroyRef))
18
+ .subscribe({ next: value => this.data.set(JSON.stringify(value)),
19
+ error: () => this.data.set('API access unavailable.') });
20
+ }
21
+ }
@@ -1,27 +1,27 @@
1
- import { isPlatformBrowser } from '@angular/common';
2
- import { inject, Injectable, PLATFORM_ID, signal } from '@angular/core';
3
- import { CanActivateFn } from '@angular/router';
4
- import { checkAccess } from '@stopnetic/js/browser';
5
-
6
- @Injectable({ providedIn: 'root' })
7
- export class StopneticAccess {
8
- readonly state = signal<'checking' | 'allowed' | 'denied'>('checking');
9
- private readonly platform = inject(PLATFORM_ID);
10
- private current?: AbortController;
11
- async check(path: string): Promise<boolean> {
12
- // Node SSR must be authorized by the SERVER guard before Angular renders.
13
- if (!isPlatformBrowser(this.platform)) { this.state.set('allowed'); return true; }
14
- this.current?.abort();
15
- const controller = new AbortController(); this.current = controller;
16
- this.state.set('checking');
17
- try {
18
- const result = await checkAccess(path.split('#')[0], { signal: controller.signal });
19
- if (controller.signal.aborted) return false;
20
- if (result.allowed) { this.state.set('allowed'); return true; }
21
- if (result.reason === 'verification_required' && result.verificationUrl) location.assign(result.verificationUrl);
22
- } catch { if (controller.signal.aborted) return false; }
23
- this.state.set('denied'); return false;
24
- }
25
- }
26
- export const stopneticRouteGuard: CanActivateFn = (_route, state) =>
27
- inject(StopneticAccess).check(state.url);
1
+ import { isPlatformBrowser } from '@angular/common';
2
+ import { inject, Injectable, PLATFORM_ID, signal } from '@angular/core';
3
+ import { CanActivateFn } from '@angular/router';
4
+ import { checkAccess } from '@stopnetic/js/browser';
5
+
6
+ @Injectable({ providedIn: 'root' })
7
+ export class StopneticAccess {
8
+ readonly state = signal<'checking' | 'allowed' | 'denied'>('checking');
9
+ private readonly platform = inject(PLATFORM_ID);
10
+ private current?: AbortController;
11
+ async check(path: string): Promise<boolean> {
12
+ // Node SSR must be authorized by the SERVER guard before Angular renders.
13
+ if (!isPlatformBrowser(this.platform)) { this.state.set('allowed'); return true; }
14
+ this.current?.abort();
15
+ const controller = new AbortController(); this.current = controller;
16
+ this.state.set('checking');
17
+ try {
18
+ const result = await checkAccess(path.split('#')[0], { signal: controller.signal });
19
+ if (controller.signal.aborted) return false;
20
+ if (result.allowed) { this.state.set('allowed'); return true; }
21
+ if (result.reason === 'verification_required' && result.verificationUrl) location.assign(result.verificationUrl);
22
+ } catch { if (controller.signal.aborted) return false; }
23
+ this.state.set('denied'); return false;
24
+ }
25
+ }
26
+ export const stopneticRouteGuard: CanActivateFn = (_route, state) =>
27
+ inject(StopneticAccess).check(state.url);
@@ -1,4 +1,4 @@
1
- import { bootstrapApplication } from '@angular/platform-browser';
2
- import { App } from './app/app';
3
- import { appConfig } from './app/app.config';
4
- bootstrapApplication(App, appConfig).catch(error => console.error(error));
1
+ import { bootstrapApplication } from '@angular/platform-browser';
2
+ import { App } from './app/app';
3
+ import { appConfig } from './app/app.config';
4
+ bootstrapApplication(App, appConfig).catch(error => console.error(error));
@@ -1 +1 @@
1
- export function loadPrivateData(signal?: AbortSignal): Promise<unknown>;
1
+ export function loadPrivateData(signal?: AbortSignal): Promise<unknown>;
@@ -1,9 +1,9 @@
1
- // Browser example. The /api/private SERVER handler must also be guarded.
2
- export async function loadPrivateData(signal) {
3
- const response = await fetch('/api/private', {
4
- credentials: 'same-origin', cache: 'no-store', signal,
5
- headers: { Accept: 'application/json' },
6
- });
7
- if (!response.ok) throw new Error(`Request failed (HTTP ${response.status}).`);
8
- return response.json();
9
- }
1
+ // Browser example. The /api/private SERVER handler must also be guarded.
2
+ export async function loadPrivateData(signal) {
3
+ const response = await fetch('/api/private', {
4
+ credentials: 'same-origin', cache: 'no-store', signal,
5
+ headers: { Accept: 'application/json' },
6
+ });
7
+ if (!response.ok) throw new Error(`Request failed (HTTP ${response.status}).`);
8
+ return response.json();
9
+ }
@@ -1,48 +1,48 @@
1
- import { useEffect, useState, type ReactNode } from 'react';
2
- import { BrowserRouter, Link, Route, Routes, useLocation } from 'react-router-dom';
3
- import { StopneticBoundary } from '@stopnetic/js/react';
4
- import { loadPrivateData } from '../load-private-data.js';
5
-
6
- function RouteGuard({ children }: { children: ReactNode }) {
7
- const location = useLocation();
8
- return <StopneticBoundary path={location.pathname + location.search}
9
- pending={null}
10
- blocked={<p>Access unavailable.</p>}>
11
- {children}
12
- </StopneticBoundary>;
13
- }
14
-
15
- function Members() {
16
- const [data, setData] = useState('Loading data...');
17
- useEffect(() => {
18
- const controller = new AbortController();
19
- loadPrivateData(controller.signal)
20
- .then(value => { if (!controller.signal.aborted) setData(JSON.stringify(value) ?? 'No data.'); })
21
- .catch(() => { if (!controller.signal.aborted) setData('API access unavailable.'); });
22
- return () => controller.abort();
23
- }, []);
24
- return <main><h1>Members</h1><p>{data}</p></main>;
25
- }
26
-
27
- function Layout() {
28
- return <>
29
- <header><nav>
30
- <Link to="/">Home</Link>{' '}<Link to="/members">Members</Link>{' '}
31
- <Link to="/slow">Slow</Link>{' '}<Link to="/blocked">Blocked</Link>{' '}
32
- <Link to="/offline">Offline</Link>
33
- </nav></header>
34
- <Routes>
35
- <Route caseSensitive path="/" element={<main><h1>Home</h1></main>}/>
36
- <Route caseSensitive path="/members" element={<Members/>}/>
37
- <Route caseSensitive path="/slow" element={<main><h1>Slow</h1></main>}/>
38
- <Route caseSensitive path="/blocked" element={<main><h1>Blocked</h1></main>}/>
39
- <Route caseSensitive path="/offline" element={<main><h1>Offline</h1></main>}/>
40
- <Route path="*" element={<main><h1>Not found</h1></main>}/>
41
- </Routes>
42
- <footer>Example site</footer>
43
- </>;
44
- }
45
-
46
- export default function App() {
47
- return <BrowserRouter><RouteGuard><Layout/></RouteGuard></BrowserRouter>;
48
- }
1
+ import { useEffect, useState, type ReactNode } from 'react';
2
+ import { BrowserRouter, Link, Route, Routes, useLocation } from 'react-router-dom';
3
+ import { StopneticBoundary } from '@stopnetic/js/react';
4
+ import { loadPrivateData } from '../load-private-data.js';
5
+
6
+ function RouteGuard({ children }: { children: ReactNode }) {
7
+ const location = useLocation();
8
+ return <StopneticBoundary path={location.pathname + location.search}
9
+ pending={null}
10
+ blocked={<p>Access unavailable.</p>}>
11
+ {children}
12
+ </StopneticBoundary>;
13
+ }
14
+
15
+ function Members() {
16
+ const [data, setData] = useState('Loading data...');
17
+ useEffect(() => {
18
+ const controller = new AbortController();
19
+ loadPrivateData(controller.signal)
20
+ .then(value => { if (!controller.signal.aborted) setData(JSON.stringify(value) ?? 'No data.'); })
21
+ .catch(() => { if (!controller.signal.aborted) setData('API access unavailable.'); });
22
+ return () => controller.abort();
23
+ }, []);
24
+ return <main><h1>Members</h1><p>{data}</p></main>;
25
+ }
26
+
27
+ function Layout() {
28
+ return <>
29
+ <header><nav>
30
+ <Link to="/">Home</Link>{' '}<Link to="/members">Members</Link>{' '}
31
+ <Link to="/slow">Slow</Link>{' '}<Link to="/blocked">Blocked</Link>{' '}
32
+ <Link to="/offline">Offline</Link>
33
+ </nav></header>
34
+ <Routes>
35
+ <Route caseSensitive path="/" element={<main><h1>Home</h1></main>}/>
36
+ <Route caseSensitive path="/members" element={<Members/>}/>
37
+ <Route caseSensitive path="/slow" element={<main><h1>Slow</h1></main>}/>
38
+ <Route caseSensitive path="/blocked" element={<main><h1>Blocked</h1></main>}/>
39
+ <Route caseSensitive path="/offline" element={<main><h1>Offline</h1></main>}/>
40
+ <Route path="*" element={<main><h1>Not found</h1></main>}/>
41
+ </Routes>
42
+ <footer>Example site</footer>
43
+ </>;
44
+ }
45
+
46
+ export default function App() {
47
+ return <BrowserRouter><RouteGuard><Layout/></RouteGuard></BrowserRouter>;
48
+ }
@@ -1,3 +1,3 @@
1
- import { createRoot } from 'react-dom/client';
2
- import App from './App';
3
- createRoot(document.getElementById('app')!).render(<App/>);
1
+ import { createRoot } from 'react-dom/client';
2
+ import App from './App';
3
+ createRoot(document.getElementById('app')!).render(<App/>);
@@ -1,54 +1,54 @@
1
- import { createEffect, createSignal, onCleanup, Show, type ParentComponent } from 'solid-js';
2
- import { A, Route, Router, useLocation } from '@solidjs/router';
3
- import { checkAccess } from '@stopnetic/js/browser';
4
- import { loadPrivateData } from '../load-private-data.js';
5
-
6
- const GuardedLayout: ParentComponent = props => {
7
- const location = useLocation();
8
- const path = () => location.pathname + location.search;
9
- const [result, setResult] = createSignal({ path: '', allowed: false, pending: true });
10
- createEffect(() => {
11
- const target = path();
12
- const controller = new AbortController();
13
- setResult({ path: target, allowed: false, pending: true });
14
- checkAccess(target, { signal: controller.signal }).then(value => {
15
- if (controller.signal.aborted) return;
16
- if (!value.allowed && value.reason === 'verification_required' && value.verificationUrl) locationRedirect(value.verificationUrl);
17
- setResult({ path: target, allowed: value.allowed, pending: false });
18
- }).catch(() => {
19
- if (!controller.signal.aborted) setResult({ path: target, allowed: false, pending: false });
20
- });
21
- onCleanup(() => controller.abort());
22
- });
23
- return <Show when={result().allowed && result().path === path() ? result() : undefined}
24
- fallback={<Show when={!result().pending && result().path === path()}>
25
- <p role="alert">Access unavailable.</p></Show>}>
26
- {_checked => <>
27
- <header><nav><A href="/">Home</A>{' '}<A href="/members">Members</A>{' '}
28
- <A href="/slow">Slow</A>{' '}<A href="/blocked">Blocked</A>{' '}<A href="/offline">Offline</A>
29
- </nav></header>
30
- {props.children}
31
- <footer>Example site</footer>
32
- </>}
33
- </Show>;
34
- };
35
- const locationRedirect = (url: string) => window.location.assign(url);
36
-
37
- function Members() {
38
- const [data, setData] = createSignal('Loading data...');
39
- const controller = new AbortController();
40
- loadPrivateData(controller.signal)
41
- .then(value => { if (!controller.signal.aborted) setData(JSON.stringify(value) ?? 'No data.'); })
42
- .catch(() => { if (!controller.signal.aborted) setData('API access unavailable.'); });
43
- onCleanup(() => controller.abort());
44
- return <main><h1>Members</h1><p>{data()}</p></main>;
45
- }
46
- export default function App() {
47
- return <Router root={GuardedLayout}>
48
- <Route path="/" component={() => <main><h1>Home</h1></main>}/>
49
- <Route path="/members" component={Members}/>
50
- <Route path="/slow" component={() => <main><h1>Slow</h1></main>}/>
51
- <Route path="/blocked" component={() => <main><h1>Blocked</h1></main>}/>
52
- <Route path="/offline" component={() => <main><h1>Offline</h1></main>}/>
53
- </Router>;
54
- }
1
+ import { createEffect, createSignal, onCleanup, Show, type ParentComponent } from 'solid-js';
2
+ import { A, Route, Router, useLocation } from '@solidjs/router';
3
+ import { checkAccess } from '@stopnetic/js/browser';
4
+ import { loadPrivateData } from '../load-private-data.js';
5
+
6
+ const GuardedLayout: ParentComponent = props => {
7
+ const location = useLocation();
8
+ const path = () => location.pathname + location.search;
9
+ const [result, setResult] = createSignal({ path: '', allowed: false, pending: true });
10
+ createEffect(() => {
11
+ const target = path();
12
+ const controller = new AbortController();
13
+ setResult({ path: target, allowed: false, pending: true });
14
+ checkAccess(target, { signal: controller.signal }).then(value => {
15
+ if (controller.signal.aborted) return;
16
+ if (!value.allowed && value.reason === 'verification_required' && value.verificationUrl) locationRedirect(value.verificationUrl);
17
+ setResult({ path: target, allowed: value.allowed, pending: false });
18
+ }).catch(() => {
19
+ if (!controller.signal.aborted) setResult({ path: target, allowed: false, pending: false });
20
+ });
21
+ onCleanup(() => controller.abort());
22
+ });
23
+ return <Show when={result().allowed && result().path === path() ? result() : undefined}
24
+ fallback={<Show when={!result().pending && result().path === path()}>
25
+ <p role="alert">Access unavailable.</p></Show>}>
26
+ {_checked => <>
27
+ <header><nav><A href="/">Home</A>{' '}<A href="/members">Members</A>{' '}
28
+ <A href="/slow">Slow</A>{' '}<A href="/blocked">Blocked</A>{' '}<A href="/offline">Offline</A>
29
+ </nav></header>
30
+ {props.children}
31
+ <footer>Example site</footer>
32
+ </>}
33
+ </Show>;
34
+ };
35
+ const locationRedirect = (url: string) => window.location.assign(url);
36
+
37
+ function Members() {
38
+ const [data, setData] = createSignal('Loading data...');
39
+ const controller = new AbortController();
40
+ loadPrivateData(controller.signal)
41
+ .then(value => { if (!controller.signal.aborted) setData(JSON.stringify(value) ?? 'No data.'); })
42
+ .catch(() => { if (!controller.signal.aborted) setData('API access unavailable.'); });
43
+ onCleanup(() => controller.abort());
44
+ return <main><h1>Members</h1><p>{data()}</p></main>;
45
+ }
46
+ export default function App() {
47
+ return <Router root={GuardedLayout}>
48
+ <Route path="/" component={() => <main><h1>Home</h1></main>}/>
49
+ <Route path="/members" component={Members}/>
50
+ <Route path="/slow" component={() => <main><h1>Slow</h1></main>}/>
51
+ <Route path="/blocked" component={() => <main><h1>Blocked</h1></main>}/>
52
+ <Route path="/offline" component={() => <main><h1>Offline</h1></main>}/>
53
+ </Router>;
54
+ }
@@ -1,3 +1,3 @@
1
- import { render } from 'solid-js/web';
2
- import App from './App';
3
- render(() => <App/>, document.getElementById('app')!);
1
+ import { render } from 'solid-js/web';
2
+ import App from './App';
3
+ render(() => <App/>, document.getElementById('app')!);
@@ -1,56 +1,56 @@
1
- <script>
2
- import { onMount } from 'svelte';
3
- import { checkAccess } from '@stopnetic/js/browser';
4
- import { loadPrivateData } from '../load-private-data.js';
5
-
6
- let path = '';
7
- let state = 'checking';
8
- let data = '';
9
- let current;
10
- async function showPage() {
11
- current?.abort();
12
- const controller = new AbortController();
13
- current = controller;
14
- path = location.pathname;
15
- state = 'checking'; data = '';
16
- try {
17
- const result = await checkAccess(path + location.search, { signal: controller.signal });
18
- if (controller.signal.aborted) return;
19
- if (!result.allowed) {
20
- if (result.reason === 'verification_required' && result.verificationUrl) location.assign(result.verificationUrl);
21
- state = 'denied'; return;
22
- }
23
- state = 'allowed';
24
- if (path === '/members') {
25
- data = 'Loading data...';
26
- try {
27
- const value = await loadPrivateData(controller.signal);
28
- if (!controller.signal.aborted) data = JSON.stringify(value);
29
- } catch { if (!controller.signal.aborted) data = 'API access unavailable.'; }
30
- }
31
- } catch { if (!controller.signal.aborted) state = 'denied'; }
32
- }
33
- function navigate(event) {
34
- if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
35
- event.preventDefault();
36
- history.pushState(null, '', event.currentTarget.href);
37
- void showPage();
38
- }
39
- onMount(() => {
40
- void showPage();
41
- addEventListener('popstate', showPage);
42
- return () => { current?.abort(); removeEventListener('popstate', showPage); };
43
- });
44
- </script>
45
-
46
- {#if state === 'allowed'}
47
- <header><nav>
48
- <a href="/" onclick={navigate}>Home</a> <a href="/members" onclick={navigate}>Members</a>
49
- <a href="/slow" onclick={navigate}>Slow</a> <a href="/blocked" onclick={navigate}>Blocked</a>
50
- <a href="/offline" onclick={navigate}>Offline</a>
51
- </nav></header>
52
- <main><h1>{path === '/members' ? 'Members' : path === '/' ? 'Home' : path === '/slow' ? 'Slow' : 'Not found'}</h1><p>{data}</p></main>
53
- <footer>Example site</footer>
54
- {:else if state === 'denied'}
55
- <p role="alert">Access unavailable.</p>
56
- {/if}
1
+ <script>
2
+ import { onMount } from 'svelte';
3
+ import { checkAccess } from '@stopnetic/js/browser';
4
+ import { loadPrivateData } from '../load-private-data.js';
5
+
6
+ let path = '';
7
+ let state = 'checking';
8
+ let data = '';
9
+ let current;
10
+ async function showPage() {
11
+ current?.abort();
12
+ const controller = new AbortController();
13
+ current = controller;
14
+ path = location.pathname;
15
+ state = 'checking'; data = '';
16
+ try {
17
+ const result = await checkAccess(path + location.search, { signal: controller.signal });
18
+ if (controller.signal.aborted) return;
19
+ if (!result.allowed) {
20
+ if (result.reason === 'verification_required' && result.verificationUrl) location.assign(result.verificationUrl);
21
+ state = 'denied'; return;
22
+ }
23
+ state = 'allowed';
24
+ if (path === '/members') {
25
+ data = 'Loading data...';
26
+ try {
27
+ const value = await loadPrivateData(controller.signal);
28
+ if (!controller.signal.aborted) data = JSON.stringify(value);
29
+ } catch { if (!controller.signal.aborted) data = 'API access unavailable.'; }
30
+ }
31
+ } catch { if (!controller.signal.aborted) state = 'denied'; }
32
+ }
33
+ function navigate(event) {
34
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
35
+ event.preventDefault();
36
+ history.pushState(null, '', event.currentTarget.href);
37
+ void showPage();
38
+ }
39
+ onMount(() => {
40
+ void showPage();
41
+ addEventListener('popstate', showPage);
42
+ return () => { current?.abort(); removeEventListener('popstate', showPage); };
43
+ });
44
+ </script>
45
+
46
+ {#if state === 'allowed'}
47
+ <header><nav>
48
+ <a href="/" onclick={navigate}>Home</a> <a href="/members" onclick={navigate}>Members</a>
49
+ <a href="/slow" onclick={navigate}>Slow</a> <a href="/blocked" onclick={navigate}>Blocked</a>
50
+ <a href="/offline" onclick={navigate}>Offline</a>
51
+ </nav></header>
52
+ <main><h1>{path === '/members' ? 'Members' : path === '/' ? 'Home' : path === '/slow' ? 'Slow' : 'Not found'}</h1><p>{data}</p></main>
53
+ <footer>Example site</footer>
54
+ {:else if state === 'denied'}
55
+ <p role="alert">Access unavailable.</p>
56
+ {/if}
@@ -1,4 +1,4 @@
1
- // Svelte 5 entry. Svelte 4 apps retain their existing `new App(...)` entry.
2
- import { mount } from 'svelte';
3
- import App from './App.svelte';
4
- mount(App, { target: document.getElementById('app') });
1
+ // Svelte 5 entry. Svelte 4 apps retain their existing `new App(...)` entry.
2
+ import { mount } from 'svelte';
3
+ import App from './App.svelte';
4
+ mount(App, { target: document.getElementById('app') });