@arcote.tech/arc-map 0.7.32 → 0.8.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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@arcote.tech/arc-map",
3
3
  "type": "module",
4
- "version": "0.7.32",
4
+ "version": "0.8.1",
5
5
  "private": false,
6
6
  "author": "Przemysław Krasiński [arcote.tech]",
7
7
  "description": "Arc Context Map — interactive map of context spaces, elements, event-flow and React useScope usages (dev tool)",
@@ -9,7 +9,8 @@
9
9
  "types": "./src/index.ts",
10
10
  "exports": {
11
11
  ".": "./src/index.ts",
12
- "./app": "./src/app/index.tsx"
12
+ "./app": "./src/app/index.tsx",
13
+ "./types": "./src/types.ts"
13
14
  },
14
15
  "scripts": {
15
16
  "type-check": "tsc --noEmit",
@@ -19,11 +20,11 @@
19
20
  "ts-morph": "^24.0.0",
20
21
  "@xyflow/react": "^12.0.0",
21
22
  "elkjs": "^0.9.3",
22
- "@arcote.tech/arc-process": "^0.7.32"
23
+ "@arcote.tech/arc-process": "^0.8.1"
23
24
  },
24
25
  "peerDependencies": {
25
- "@arcote.tech/arc": "^0.7.32",
26
- "@arcote.tech/platform": "^0.7.32",
26
+ "@arcote.tech/arc": "^0.8.1",
27
+ "@arcote.tech/platform": "^0.8.1",
27
28
  "react": "^18.0.0 || ^19.0.0",
28
29
  "react-dom": "^18.0.0 || ^19.0.0",
29
30
  "typescript": "^5.0.0"
package/src/app/api.ts ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Fetch do API serwera mapy z tokenem parowania. Serwer (gdy uruchomiony
3
+ * z `authToken`) wymaga `X-Arc-Map-Token` na `/api/*`; własny front dostaje
4
+ * token wstrzyknięty do shella jako `window.__ARC_MAP_TOKEN__`. SSE nie umie
5
+ * nagłówków — tam token idzie w query (`sseUrl`).
6
+ */
7
+
8
+ declare global {
9
+ interface Window {
10
+ __ARC_MAP_TOKEN__?: string;
11
+ }
12
+ }
13
+
14
+ const token = (): string | undefined =>
15
+ typeof window === "undefined" ? undefined : window.__ARC_MAP_TOKEN__;
16
+
17
+ export function apiFetch(path: string, init?: RequestInit): Promise<Response> {
18
+ const t = token();
19
+ const headers = new Headers(init?.headers);
20
+ if (t) headers.set("X-Arc-Map-Token", t);
21
+ return fetch(path, { ...init, headers });
22
+ }
23
+
24
+ /** URL strumienia SSE z tokenem w query (EventSource nie wysyła nagłówków). */
25
+ export function sseUrl(path: string): string {
26
+ const t = token();
27
+ return t ? `${path}${path.includes("?") ? "&" : "?"}token=${encodeURIComponent(t)}` : path;
28
+ }
package/src/app/index.tsx CHANGED
@@ -4,6 +4,7 @@ import "@xyflow/react/dist/style.css";
4
4
  import { StrictMode, useEffect, useState } from "react";
5
5
  import { createRoot } from "react-dom/client";
6
6
  import type { MapJson, UseCaseEntry } from "../types";
7
+ import { apiFetch, sseUrl } from "./api";
7
8
  import { MapView } from "./map-view";
8
9
  import { UseCaseView } from "./usecase-view";
9
10
  import { GLASS } from "./theme";
@@ -54,11 +55,11 @@ function App() {
54
55
  const [mode, setMode] = useState<Mode>("use-cases");
55
56
 
56
57
  const load = () => {
57
- fetch("/api/map")
58
+ apiFetch("/api/map")
58
59
  .then((r) => r.json())
59
60
  .then((data) => (data?.error ? setError(data.error) : (setMap(data), setError(null))))
60
61
  .catch((e) => setError(String(e)));
61
- fetch("/api/use-cases")
62
+ apiFetch("/api/use-cases")
62
63
  .then((r) => r.json())
63
64
  .then((d) => setUseCases(d.useCases ?? []))
64
65
  .catch(() => setUseCases([]));
@@ -66,7 +67,7 @@ function App() {
66
67
 
67
68
  useEffect(() => {
68
69
  load();
69
- const es = new EventSource("/api/reload-stream");
70
+ const es = new EventSource(sseUrl("/api/reload-stream"));
70
71
  es.onmessage = (ev) => {
71
72
  if (ev.data && ev.data !== "connected") load();
72
73
  };
@@ -16,6 +16,7 @@ import {
16
16
  } from "@arcote.tech/arc-process";
17
17
  import { ProcessDiagram, type SourceProvider } from "@arcote.tech/arc-process/react";
18
18
  import type { MapJson, UseCaseEntry } from "../types";
19
+ import { apiFetch } from "./api";
19
20
  import { UseCaseTree } from "./usecase-tree";
20
21
  import { GLASS } from "./theme";
21
22
 
@@ -24,7 +25,7 @@ const sourceProvider: SourceProvider = async (node: ProcessNode) => {
24
25
  const source = (node.meta as { source?: { file: string; line?: number } })
25
26
  ?.source;
26
27
  if (!source?.file) return null;
27
- const res = await fetch(`/api/source?file=${encodeURIComponent(source.file)}`);
28
+ const res = await apiFetch(`/api/source?file=${encodeURIComponent(source.file)}`);
28
29
  if (!res.ok) return null;
29
30
  return { file: source.file, line: source.line, code: await res.text() };
30
31
  };
@@ -42,7 +43,7 @@ export function UseCaseView({ map, useCases }: { map: MapJson; useCases: UseCase
42
43
  useEffect(() => {
43
44
  if (!selectedId) return;
44
45
  setMdx("");
45
- fetch(`/api/use-cases/raw?id=${encodeURIComponent(selectedId)}`)
46
+ apiFetch(`/api/use-cases/raw?id=${encodeURIComponent(selectedId)}`)
46
47
  .then((r) => (r.ok ? r.text() : Promise.reject(new Error(`HTTP ${r.status}`))))
47
48
  .then(setMdx)
48
49
  .catch((e) => setMdx(`# Błąd\n\nNie udało się wczytać: ${String(e)}`));
package/src/mapper.ts CHANGED
@@ -29,6 +29,7 @@ import {
29
29
  type MapElement,
30
30
  type MapElementMethod,
31
31
  type MapJson,
32
+ type MapMeta,
32
33
  type MapSpace,
33
34
  } from "./types";
34
35
 
@@ -52,6 +53,8 @@ export interface BuildMapInput {
52
53
  catalog?: CatalogEntry[];
53
54
  /** ISO timestamp; injected so the function stays deterministic for tests. */
54
55
  generatedAt?: string;
56
+ /** Tożsamość aplikacji + wersja kontraktu — przechodzi 1:1 do `MapJson.meta`. */
57
+ meta?: MapMeta;
55
58
  }
56
59
 
57
60
  // ---------------------------------------------------------------------------
@@ -400,6 +403,7 @@ export function buildMapJson(input: BuildMapInput): MapJson {
400
403
  const resolvedComponents = components.filter((c) => usedComponentIds.has(c.id));
401
404
 
402
405
  return {
406
+ meta: input.meta,
403
407
  spaces: [...spaceIndex.values()],
404
408
  elements,
405
409
  edges,
package/src/server.ts CHANGED
@@ -21,7 +21,7 @@ import { buildMapJson, type ModuleInfo } from "./mapper";
21
21
  import { loadCatalog } from "./catalog";
22
22
  import { scanScopeUsages, type ScanInput } from "./scan";
23
23
  import { discoverUseCases, type UseCasePackage } from "./use-cases/discover";
24
- import type { MapJson, UseCaseEntry } from "./types";
24
+ import type { MapJson, MapMeta, UseCaseEntry } from "./types";
25
25
 
26
26
  export interface MapServerOptions {
27
27
  /** Returns the current live context (recomputed by the host on rebuild). */
@@ -39,6 +39,28 @@ export interface MapServerOptions {
39
39
  appEntry?: string;
40
40
  /** Packages to scan for `use-cases/**\/*.mdx`. Enables the use-case mode. */
41
41
  useCasePackages?: UseCasePackage[];
42
+ /**
43
+ * Pairing token. When set, every `/api/*` request (except OPTIONS) must
44
+ * carry `X-Arc-Map-Token: <token>` — otherwise 401. `/api/reload-stream`
45
+ * additionally accepts `?token=` (EventSource cannot send headers). The
46
+ * map's own front gets the token injected into the shell HTML (served
47
+ * locally only), so its UX is unchanged. This closes the standing hole
48
+ * where ANY web page open in the developer's browser could read the whole
49
+ * map and source code via CORS `*`.
50
+ */
51
+ authToken?: string;
52
+ /** Tożsamość aplikacji + wersja kontraktu — trafia do `MapJson.meta`. */
53
+ meta?: MapMeta;
54
+ /**
55
+ * Discovery (spec/platform-headless.md): informacje o serwerze PLATFORMY
56
+ * tej aplikacji (komendy/query/WS + chunki federowane) — konsument
57
+ * (host federacji, np. BMF) dostaje je z `GET /api/discovery`.
58
+ */
59
+ platform?: {
60
+ platformUrl: string;
61
+ federation?: unknown;
62
+ peers?: Readonly<Record<string, string>>;
63
+ };
42
64
  }
43
65
 
44
66
  export interface MapServer {
@@ -53,13 +75,19 @@ export interface MapServer {
53
75
  const DEFAULT_PORT = 5006;
54
76
  const MAX_PORT_TRIES = 20;
55
77
 
56
- function shellHtml(): string {
78
+ function shellHtml(authToken?: string): string {
79
+ // Token dla własnego frontu mapy — shell serwowany jest tylko lokalnie
80
+ // (localhost), więc wstrzyknięcie nie poszerza powierzchni: kto może
81
+ // odczytać ten HTML, ten i tak siedzi na maszynie z serwerem.
82
+ const tokenScript = authToken
83
+ ? `\n <script>window.__ARC_MAP_TOKEN__ = ${JSON.stringify(authToken)};</script>`
84
+ : "";
57
85
  return `<!DOCTYPE html>
58
86
  <html lang="en">
59
87
  <head>
60
88
  <meta charset="UTF-8" />
61
89
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
62
- <title>Arc Context Map</title>
90
+ <title>Arc Context Map</title>${tokenScript}
63
91
  <link rel="stylesheet" href="/app.css" />
64
92
  <link rel="preconnect" href="https://fonts.googleapis.com" />
65
93
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" />
@@ -110,7 +138,17 @@ async function buildApp(appEntry: string): Promise<{ js: string; css: string }>
110
138
  const CORS = {
111
139
  "Access-Control-Allow-Origin": "*",
112
140
  "Access-Control-Allow-Methods": "GET, OPTIONS",
113
- "Access-Control-Allow-Headers": "Content-Type",
141
+ "Access-Control-Allow-Headers": "Content-Type, X-Arc-Map-Token",
142
+ };
143
+
144
+ /**
145
+ * Preflight z publicznej strony (https) do localhost wymaga w Chrome zgody
146
+ * Private Network Access — bez tego nagłówka fetch z hostowanego BMF do
147
+ * lokalnego serwera mapy jest blokowany zanim w ogóle dojdzie do requestu.
148
+ */
149
+ const PREFLIGHT = {
150
+ ...CORS,
151
+ "Access-Control-Allow-Private-Network": "true",
114
152
  };
115
153
 
116
154
  export async function startMapServer(opts: MapServerOptions): Promise<MapServer> {
@@ -140,7 +178,14 @@ export async function startMapServer(opts: MapServerOptions): Promise<MapServer>
140
178
  const buildPayload = (): MapJson => {
141
179
  const context = opts.getContext();
142
180
  if (!context) {
143
- return { spaces: [], elements: [], edges: [], components: [], componentEdges: [] };
181
+ return {
182
+ meta: opts.meta,
183
+ spaces: [],
184
+ elements: [],
185
+ edges: [],
186
+ components: [],
187
+ componentEdges: [],
188
+ };
144
189
  }
145
190
  const scan = getScan();
146
191
  const moduleByElement = (opts.getModuleIndex ?? buildModuleIndex)();
@@ -152,6 +197,7 @@ export async function startMapServer(opts: MapServerOptions): Promise<MapServer>
152
197
  pages: buildPageNodes(),
153
198
  catalog: getCatalog(),
154
199
  generatedAt: new Date().toISOString(),
200
+ meta: opts.meta,
155
201
  });
156
202
  };
157
203
 
@@ -178,7 +224,24 @@ export async function startMapServer(opts: MapServerOptions): Promise<MapServer>
178
224
 
179
225
  const handler = (req: Request): Response => {
180
226
  const url = new URL(req.url);
181
- if (req.method === "OPTIONS") return new Response(null, { headers: CORS });
227
+ if (req.method === "OPTIONS") return new Response(null, { headers: PREFLIGHT });
228
+
229
+ // Guard tokenu parowania na całym /api/* (patrz MapServerOptions.authToken).
230
+ // 401 wychodzi Z nagłówkami CORS, żeby zewnętrzny konsument (BMF) umiał
231
+ // odróżnić „zły token" od „serwer nie żyje" (network error).
232
+ if (opts.authToken && url.pathname.startsWith("/api/")) {
233
+ const header = req.headers.get("X-Arc-Map-Token");
234
+ const query =
235
+ url.pathname === "/api/reload-stream"
236
+ ? url.searchParams.get("token")
237
+ : null;
238
+ if (header !== opts.authToken && query !== opts.authToken) {
239
+ return new Response(JSON.stringify({ error: "invalid pairing token" }), {
240
+ status: 401,
241
+ headers: { ...CORS, "Content-Type": "application/json" },
242
+ });
243
+ }
244
+ }
182
245
 
183
246
  switch (url.pathname) {
184
247
  case "/app.js":
@@ -203,6 +266,25 @@ export async function startMapServer(opts: MapServerOptions): Promise<MapServer>
203
266
  headers: { ...CORS, "Content-Type": "application/json", "Cache-Control": "no-cache" },
204
267
  });
205
268
  }
269
+ case "/api/discovery": {
270
+ // Discovery dla zewnętrznego konsumenta (za tokenem parowania):
271
+ // gdzie żyje serwer platformy, czy federacja jest dostępna i z
272
+ // jakimi peers (polityka kompatybilności hosta) — bez konieczności
273
+ // zgadywania portów.
274
+ const platform = opts.platform;
275
+ const body = {
276
+ meta: opts.meta ?? null,
277
+ platformUrl: platform?.platformUrl ?? null,
278
+ federation: (platform?.federation as object | undefined) ?? null,
279
+ peers: platform?.peers ?? null,
280
+ federationManifestUrl: platform
281
+ ? `${platform.platformUrl}/api/federation/manifest`
282
+ : null,
283
+ };
284
+ return new Response(JSON.stringify(body), {
285
+ headers: { ...CORS, "Content-Type": "application/json", "Cache-Control": "no-cache" },
286
+ });
287
+ }
206
288
  case "/api/use-cases": {
207
289
  const list: UseCaseEntry[] = getUseCases().map(
208
290
  ({ absPath, ...e }) => e,
@@ -278,7 +360,7 @@ export async function startMapServer(opts: MapServerOptions): Promise<MapServer>
278
360
  });
279
361
  }
280
362
  default:
281
- return new Response(shellHtml(), {
363
+ return new Response(shellHtml(opts.authToken), {
282
364
  headers: { "Content-Type": "text/html" },
283
365
  });
284
366
  }
package/src/types.ts CHANGED
@@ -150,8 +150,25 @@ export interface MapComponentEdge {
150
150
  line?: number;
151
151
  }
152
152
 
153
+ /**
154
+ * Meta zbudowanej mapy — tożsamość aplikacji dla zewnętrznych konsumentów
155
+ * (np. import kontekstu do BMF). `schemaVersion` wersjonuje kontrakt MapJson;
156
+ * konsument odrzuca nieznaną wersję major z czytelnym komunikatem.
157
+ */
158
+ export interface MapMeta {
159
+ schemaVersion: number;
160
+ /** Nazwa aplikacji (root package.json workspace'u). */
161
+ name: string;
162
+ version?: string;
163
+ }
164
+
165
+ /** Bieżąca wersja kontraktu MapJson serwowanego przez `/api/map`. */
166
+ export const MAP_SCHEMA_VERSION = 1;
167
+
153
168
  /** Full payload returned by `GET /api/map`. */
154
169
  export interface MapJson {
170
+ /** Tożsamość aplikacji + wersja kontraktu (dla zewnętrznych konsumentów). */
171
+ meta?: MapMeta;
155
172
  spaces: MapSpace[];
156
173
  elements: MapElement[];
157
174
  edges: MapEdge[];
@@ -0,0 +1,2 @@
1
+ // Minimal front entry for server tests — avoids bundling the real React app.
2
+ export {};
@@ -0,0 +1,110 @@
1
+ import { afterAll, describe, expect, test } from "bun:test";
2
+ import { join } from "node:path";
3
+ import { startMapServer, type MapServer } from "../src/server";
4
+
5
+ /**
6
+ * Testy parowania tokenem serwera arc-map (konsumpcja przez zewnętrzną
7
+ * aplikację, np. BMF): guard `X-Arc-Map-Token` na `/api/*`, nagłówki CORS +
8
+ * Private Network Access na preflight, fallback `?token=` na SSE oraz meta
9
+ * aplikacji w `/api/map`.
10
+ */
11
+
12
+ const APP_ENTRY = join(import.meta.dir, "fixtures/app-entry.ts");
13
+ const TOKEN = "sekret-parowania-123";
14
+
15
+ const servers: MapServer[] = [];
16
+ afterAll(() => {
17
+ for (const s of servers) s.stop();
18
+ });
19
+
20
+ async function boot(opts: { authToken?: string } = {}): Promise<MapServer> {
21
+ const server = await startMapServer({
22
+ getContext: () => null,
23
+ scan: { globs: [], workspaceRoot: import.meta.dir, packageOf: () => undefined },
24
+ port: 45810 + servers.length * 25,
25
+ appEntry: APP_ENTRY,
26
+ meta: { schemaVersion: 1, name: "@x/test-app", version: "1.2.3" },
27
+ ...opts,
28
+ });
29
+ servers.push(server);
30
+ return server;
31
+ }
32
+
33
+ describe("startMapServer bez authToken (backward compat)", () => {
34
+ test("/api/map odpowiada bez tokenu", async () => {
35
+ const s = await boot();
36
+ const res = await fetch(`${s.url}/api/map`);
37
+ expect(res.status).toBe(200);
38
+ const body = (await res.json()) as { meta?: { name: string } };
39
+ expect(body.meta?.name).toBe("@x/test-app");
40
+ });
41
+ });
42
+
43
+ describe("startMapServer z authToken", () => {
44
+ let s: MapServer;
45
+ test("boot", async () => {
46
+ s = await boot({ authToken: TOKEN });
47
+ });
48
+
49
+ test("/api/map bez tokenu → 401 z nagłówkami CORS", async () => {
50
+ const res = await fetch(`${s.url}/api/map`);
51
+ expect(res.status).toBe(401);
52
+ expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*");
53
+ });
54
+
55
+ test("/api/map ze złym tokenem → 401", async () => {
56
+ const res = await fetch(`${s.url}/api/map`, {
57
+ headers: { "X-Arc-Map-Token": "zly" },
58
+ });
59
+ expect(res.status).toBe(401);
60
+ });
61
+
62
+ test("/api/map z tokenem → 200 + meta", async () => {
63
+ const res = await fetch(`${s.url}/api/map`, {
64
+ headers: { "X-Arc-Map-Token": TOKEN },
65
+ });
66
+ expect(res.status).toBe(200);
67
+ const body = (await res.json()) as {
68
+ meta?: { schemaVersion: number; name: string; version?: string };
69
+ };
70
+ expect(body.meta).toEqual({
71
+ schemaVersion: 1,
72
+ name: "@x/test-app",
73
+ version: "1.2.3",
74
+ });
75
+ });
76
+
77
+ test("OPTIONS przechodzi bez tokenu: PNA + Allow-Headers z X-Arc-Map-Token", async () => {
78
+ const res = await fetch(`${s.url}/api/map`, { method: "OPTIONS" });
79
+ expect(res.status).toBeLessThan(300);
80
+ expect(res.headers.get("Access-Control-Allow-Private-Network")).toBe("true");
81
+ expect(res.headers.get("Access-Control-Allow-Headers")).toContain(
82
+ "X-Arc-Map-Token",
83
+ );
84
+ });
85
+
86
+ test("/api/reload-stream przyjmuje ?token= (EventSource bez nagłówków)", async () => {
87
+ const res = await fetch(
88
+ `${s.url}/api/reload-stream?token=${encodeURIComponent(TOKEN)}`,
89
+ );
90
+ expect(res.status).toBe(200);
91
+ expect(res.headers.get("Content-Type")).toContain("text/event-stream");
92
+ const reader = res.body!.getReader();
93
+ const first = await reader.read();
94
+ expect(new TextDecoder().decode(first.value)).toContain("connected");
95
+ await reader.cancel();
96
+ });
97
+
98
+ test("/api/reload-stream bez tokenu → 401", async () => {
99
+ const res = await fetch(`${s.url}/api/reload-stream`);
100
+ expect(res.status).toBe(401);
101
+ });
102
+
103
+ test("shell HTML (/) działa bez tokenu i wstrzykuje token dla własnego frontu", async () => {
104
+ const res = await fetch(`${s.url}/`);
105
+ expect(res.status).toBe(200);
106
+ const html = await res.text();
107
+ expect(html).toContain("__ARC_MAP_TOKEN__");
108
+ expect(html).toContain(TOKEN);
109
+ });
110
+ });
@@ -0,0 +1,48 @@
1
+ ---
2
+ title: Parowanie zewnętrznego konsumenta tokenem
3
+ description: Serwer mapy chroni /api/* tokenem parowania; zewnętrzna aplikacja (np. BMF) wysyła go w nagłówku X-Arc-Map-Token
4
+ order: 1
5
+ ---
6
+
7
+ ## Cel
8
+
9
+ Zamknąć API serwera mapy (kontekst + kod źródłowy przez `/api/source`) przed
10
+ obcymi stronami w przeglądarce dewelopera, a jednocześnie dać zewnętrznym
11
+ konsumentom (BMF) prosty sposób podłączenia się: token wypisany w konsoli
12
+ (wzór Jupyter).
13
+
14
+ ## Aktorzy
15
+
16
+ - **Deweloper** — uruchamia `arc platform dev --map`, wkleja token w BMF.
17
+ - **Zewnętrzny konsument (BMF)** — fetchuje `/api/map`, `/api/source`,
18
+ `/api/use-cases`, SSE `/api/reload-stream` z tokenem.
19
+ - **Obca strona w przeglądarce** — bez tokenu dostaje 401.
20
+
21
+ ## Przepływ kroków
22
+
23
+ <Step id="start" title="Start serwera z tokenem">
24
+ Deweloper uruchamia `arc platform dev --map`. CLI generuje token parowania
25
+ (`ARC_MAP_TOKEN` w env → token stały) i wypisuje go w konsoli obok adresu mapy.
26
+ Serwer dostaje `authToken` oraz `meta` (nazwa + wersja aplikacji z root
27
+ `package.json`, `schemaVersion` kontraktu MapJson).
28
+ </Step>
29
+
30
+ <Step id="wlasny-front" title="Własny front mapy działa bez zmian">
31
+ Shell HTML (serwowany tylko lokalnie) niesie wstrzyknięty
32
+ `window.__ARC_MAP_TOKEN__`; helper `apiFetch` dokłada nagłówek
33
+ `X-Arc-Map-Token`, a `EventSource` dostaje token w query (`sseUrl`).
34
+ </Step>
35
+
36
+ <Step id="konsument" title="Parowanie zewnętrznego konsumenta">
37
+ Deweloper wkleja adres i token w BMF. Konsument wysyła
38
+ `X-Arc-Map-Token` na każde `/api/*`; preflight OPTIONS przechodzi bez tokenu
39
+ i odpowiada nagłówkami CORS + `Access-Control-Allow-Private-Network: true`
40
+ (Chrome PNA dla https→localhost). `GET /api/map` zwraca `meta` — konsument
41
+ waliduje `schemaVersion` i pokazuje nazwę aplikacji.
42
+ </Step>
43
+
44
+ <Step id="odmowa" title="Odmowa bez tokenu">
45
+ Request na `/api/*` bez tokenu lub ze złym tokenem dostaje **401 z nagłówkami
46
+ CORS** — konsument odróżnia „zły token" od „serwer nie żyje" (network error).
47
+ Strony spoza parowania nie odczytają mapy ani kodu źródłowego.
48
+ </Step>