@braidlabs/console 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 (41) hide show
  1. package/README.md +131 -0
  2. package/package.json +43 -0
  3. package/src/app/console-app.d.ts +17 -0
  4. package/src/app/console-app.js +48 -0
  5. package/src/app/console-app.js.map +1 -0
  6. package/src/app/main.d.ts +1 -0
  7. package/src/app/main.js +32 -0
  8. package/src/app/main.js.map +1 -0
  9. package/src/index.d.ts +38 -0
  10. package/src/index.js +30 -0
  11. package/src/index.js.map +1 -0
  12. package/src/lib/access-panel.d.ts +21 -0
  13. package/src/lib/access-panel.js +35 -0
  14. package/src/lib/access-panel.js.map +1 -0
  15. package/src/lib/client.d.ts +94 -0
  16. package/src/lib/client.js +109 -0
  17. package/src/lib/client.js.map +1 -0
  18. package/src/lib/draft.d.ts +45 -0
  19. package/src/lib/draft.js +85 -0
  20. package/src/lib/draft.js.map +1 -0
  21. package/src/lib/integration-panel.d.ts +13 -0
  22. package/src/lib/integration-panel.js +85 -0
  23. package/src/lib/integration-panel.js.map +1 -0
  24. package/src/lib/integration.d.ts +36 -0
  25. package/src/lib/integration.js +148 -0
  26. package/src/lib/integration.js.map +1 -0
  27. package/src/lib/registry-console.d.ts +46 -0
  28. package/src/lib/registry-console.js +88 -0
  29. package/src/lib/registry-console.js.map +1 -0
  30. package/src/lib/registry-editor.d.ts +26 -0
  31. package/src/lib/registry-editor.js +133 -0
  32. package/src/lib/registry-editor.js.map +1 -0
  33. package/src/lib/styles.d.ts +27 -0
  34. package/src/lib/styles.js +470 -0
  35. package/src/lib/styles.js.map +1 -0
  36. package/src/lib/topology-graph.d.ts +47 -0
  37. package/src/lib/topology-graph.js +124 -0
  38. package/src/lib/topology-graph.js.map +1 -0
  39. package/src/lib/topology.d.ts +86 -0
  40. package/src/lib/topology.js +122 -0
  41. package/src/lib/topology.js.map +1 -0
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @braidlabs/console
2
+
3
+ A read-only console for a [Braid](../braid-gateway) gateway's fragment registry. One package, two
4
+ shapes: a mountable React component, and a standalone deployable app.
5
+
6
+ **It needs nothing deployed.** It reads the `/__braid/registry` discovery endpoint the gateway
7
+ already serves, so it works against a gateway whose manifests are defined in code — which is every
8
+ gateway today. No snapshot store, no write API.
9
+
10
+ ## As a library
11
+
12
+ For teams who already run an internal admin app and want this inside it rather than beside it:
13
+
14
+ ```tsx
15
+ import { RegistryConsole } from '@braidlabs/console';
16
+
17
+ <RegistryConsole api={{ baseUrl: '/api/gateway', headers: () => ({ Authorization: token }) }} />;
18
+ ```
19
+
20
+ | Prop | Type | Notes |
21
+ | --- | --- | --- |
22
+ | `api` | `ConsoleApi` | base URL, discovery path, a `headers()` hook, or your own `fetch` |
23
+ | `theme` | `'light' \| 'dark'` | omit to follow the viewer's OS setting |
24
+ | `className` | `string` | for sizing and positioning |
25
+ | `onLoaded` | `(listing) => void` | for hosts that want their own chrome |
26
+
27
+ ## As an app
28
+
29
+ ```bash
30
+ nx build-app braid-console # → dist/apps/braid-console
31
+ nx serve-app braid-console # dev server
32
+ ```
33
+
34
+ A static bundle for object storage, nginx, or the gateway itself. Configuration comes from the DOM,
35
+ not from a build-time constant, so **one artifact serves every environment**:
36
+
37
+ ```html
38
+ <script type="application/json" id="braid-console-config">
39
+ { "baseUrl": "https://shell.example" }
40
+ </script>
41
+ ```
42
+
43
+ Omit it and the console reads the origin it was served from — which is what you want when the
44
+ gateway serves the bundle, and the simplest deployment there is.
45
+
46
+ ## Being a library has consequences
47
+
48
+ Mounting inside someone else's page is a real constraint, and these are cheaper to honor from the
49
+ start than to retrofit:
50
+
51
+ **No global styles.** Every rule is scoped under `.braid-console`. No resets, no bare element
52
+ selectors, no `:root`. A library that styles `body` or `*` vandalizes its host, and the damage
53
+ surfaces in a part of the app nobody connects to this component. There is a test that walks the
54
+ parsed CSSOM and fails if any selector escapes the scope.
55
+
56
+ **No router ownership.** The console cannot own the URL bar in someone else's app, so it holds its
57
+ own state.
58
+
59
+ **No session.** It takes an API base and a `headers()` hook and never performs a login — identity
60
+ belongs to the host, exactly as it does for the gateway's own `principal` resolver.
61
+
62
+ **Theming by variable, not by specificity.** Everything themable is a CSS custom property on the
63
+ scope class. Dark mode follows `prefers-color-scheme`; `theme="dark"` forces it, and wins in both
64
+ directions, because an admin shell may be dark inside a light OS.
65
+
66
+ **Container queries, not media queries.** The console's width is its host's business, not the
67
+ viewport's. In a 420px sidebar it stacks — on a 1280px screen.
68
+
69
+ **Slim is a number.** 140 kB gzipped for the app bundle including React and the inlined
70
+ stylesheet, enforced by `nx size braid-console`. Currently 61 kB. React is a peer dependency in
71
+ library form, so a host never ships two copies.
72
+
73
+ ## Editing
74
+
75
+ ```tsx
76
+ import { RegistryEditor } from '@braidlabs/console';
77
+
78
+ <RegistryEditor api={{ apiPath: '/__braid/registry-api' }} onPublished={(o) => toast(o.snapshot.id)} />;
79
+ ```
80
+
81
+ Needs [`@braidlabs/registry`](../braid-registry)'s write API mounted. The flow is **branch from
82
+ what is pinned → edit → see what it changes → publish**, and publishing never mutates the snapshot
83
+ being edited: it mints a new one and moves a pointer, so the previous configuration survives
84
+ untouched and rollback is re-pinning it.
85
+
86
+ Three things the editor does on purpose:
87
+
88
+ **Validates as you type, and does not trust itself.** The same `validateRegistry` the server runs,
89
+ run locally for the person typing. The server validates again, because a client check is advice and
90
+ the server's is the decision — a 422 shows the server's findings, not the client's.
91
+
92
+ **Marks gateway-owned fields.** `endpoint`, `pierce`, `access`, `fallback` carry a badge, and the
93
+ pre-publish diff labels every change by owner. A changed `pierce` is a routing change with
94
+ page-wide blast radius; a changed `title` is a label. An unlabelled list makes them look alike.
95
+
96
+ **Clearing a field removes it.** An empty input means *unset*, not `""` — because an omitted field
97
+ is what lets a fragment descriptor supply it later, and `title: ""` is not the same as no title.
98
+
99
+ **Previews who loses access.** *Show access* renders a list/fetch matrix per named principal, with
100
+ `✓ → ✗` transitions against what is pinned. Losses lead; the grid is context. The count surfaces in
101
+ the publish bar whether or not the panel is open, because a finding hidden behind a toggle nobody
102
+ pressed is not a finding. Every outcome carries a text label as well as a symbol, so the meaning
103
+ does not depend on colour.
104
+
105
+ Principals are what-ifs, not a directory — the gateway has none. Pass `principals` and
106
+ `onPrincipalsChange` to hold them in the host; omit both and the editor keeps them for the session.
107
+
108
+ Drafts live in the browser. Nothing is written until you publish, so closing the tab discards, and
109
+ there is no draft state to reconcile between editors.
110
+
111
+ ## What it shows
112
+
113
+ | Column | |
114
+ | --- | --- |
115
+ | Fragment | id, title, description, tags |
116
+ | Adapter | `compat`, `custom-element`, … |
117
+ | Pierces | the page URL patterns it is server-rendered into |
118
+ | Mount | what `<fragment-slot name>` resolves to |
119
+ | Access | `loadable` or `gated` |
120
+
121
+ `gated` is not a broken row. Listing and loading are **separate rules**, so a fragment you may see
122
+ but not load is a legitimate state — a launcher should render it, not hide it. And when a gateway
123
+ is in development mode the listing skips access filtering entirely, which the console says out
124
+ loud rather than letting you mistake a dev listing for a production one.
125
+
126
+ ## Related
127
+
128
+ - [`@braidlabs/registry`](../braid-registry) — snapshots and analysis, for when the registry
129
+ becomes editable
130
+ - [the plan](../../docs/plans/braid-registry-console-plan.md) — including why the console is
131
+ deliberately optional, and the line it must not cross
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@braidlabs/console",
3
+ "version": "0.1.1",
4
+ "description": "A read-only console for a Braid gateway's fragment registry. Mountable React library, or a standalone deployable app.",
5
+ "keywords": [
6
+ "braid",
7
+ "micro-frontends",
8
+ "registry",
9
+ "console",
10
+ "react"
11
+ ],
12
+ "license": "MIT",
13
+ "sideEffects": false,
14
+ "type": "module",
15
+ "main": "./src/index.js",
16
+ "types": "./src/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./src/index.d.ts",
20
+ "default": "./src/index.js"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "peerDependencies": {
25
+ "@braidlabs/gateway": "^0.1.1",
26
+ "react": "^18.0.0 || ^19.0.0",
27
+ "react-dom": "^18.0.0 || ^19.0.0",
28
+ "@braidlabs/registry": "^0.1.1"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/braidjs/braid.git",
36
+ "directory": "libs/braid-console"
37
+ },
38
+ "homepage": "https://github.com/braidjs/braid#readme",
39
+ "bugs": {
40
+ "url": "https://github.com/braidjs/braid/issues"
41
+ },
42
+ "module": "./src/index.js"
43
+ }
@@ -0,0 +1,17 @@
1
+ import type { ConsoleApi } from '../lib/client.js';
2
+ /**
3
+ * The standalone deployment's shell.
4
+ *
5
+ * This lives in `app/`, not `lib/`, on purpose: it owns a page — a title, a tab strip, and the
6
+ * URL — and every one of those is something the library must *not* own when it mounts inside
7
+ * someone else's admin app. Keeping the page-level concerns here is what lets `RegistryConsole`
8
+ * stay embeddable.
9
+ *
10
+ * The view is in the URL hash rather than in state alone, so a topology someone is looking at is
11
+ * a link they can paste into a ticket. That is the difference between a console an operator uses
12
+ * and one they screenshot.
13
+ */
14
+ export declare function ConsoleApp({ api, edit }: {
15
+ api: ConsoleApi;
16
+ edit: boolean;
17
+ }): import("react").JSX.Element;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useState } from 'react';
3
+ import { RegistryConsole } from '../lib/registry-console.js';
4
+ import { RegistryEditor } from '../lib/registry-editor.js';
5
+ import { ensureStyles } from '../lib/styles.js';
6
+ /**
7
+ * The standalone deployment's shell.
8
+ *
9
+ * This lives in `app/`, not `lib/`, on purpose: it owns a page — a title, a tab strip, and the
10
+ * URL — and every one of those is something the library must *not* own when it mounts inside
11
+ * someone else's admin app. Keeping the page-level concerns here is what lets `RegistryConsole`
12
+ * stay embeddable.
13
+ *
14
+ * The view is in the URL hash rather than in state alone, so a topology someone is looking at is
15
+ * a link they can paste into a ticket. That is the difference between a console an operator uses
16
+ * and one they screenshot.
17
+ */
18
+ export function ConsoleApp({ api, edit }) {
19
+ const [view, setView] = useState(() => readView(edit));
20
+ useEffect(() => {
21
+ ensureStyles();
22
+ }, []);
23
+ // Back and forward should move between views, since the tabs put entries in history.
24
+ useEffect(() => {
25
+ const onHashChange = () => setView(readView(edit));
26
+ window.addEventListener('hashchange', onHashChange);
27
+ return () => window.removeEventListener('hashchange', onHashChange);
28
+ }, [edit]);
29
+ function go(next) {
30
+ window.location.hash = next;
31
+ setView(next);
32
+ }
33
+ const tabs = [
34
+ { id: 'fragments', label: 'Fragments' },
35
+ { id: 'topology', label: 'Topology' },
36
+ ...(edit ? [{ id: 'edit', label: 'Edit registry' }] : []),
37
+ ];
38
+ return (_jsxs("div", { className: "braid-console", children: [_jsxs("div", { className: "braid-console__shell", children: [_jsxs("div", { className: "braid-console__titlebar", children: [_jsx("h1", { className: "braid-console__producttitle", children: "Braid gateway" }), _jsxs("span", { className: "braid-console__breadcrumb", children: [_jsx("span", { "aria-hidden": "true", children: "\u00B7" }), _jsx("code", { children: api.baseUrl || window.location.host })] })] }), _jsx("div", { className: "braid-console__commandbar", children: _jsx("div", { className: "braid-console__tabs", role: "tablist", "aria-label": "Console views", children: tabs.map((entry) => (_jsx("button", { type: "button", role: "tab", className: "braid-console__tab", "aria-selected": view === entry.id, onClick: () => go(entry.id), children: entry.label }, entry.id))) }) })] }), view === 'edit' ? (_jsx(RegistryEditor, { api: api })) : (_jsx(RegistryConsole, { api: api, chrome: false, view: view }))] }));
39
+ }
40
+ function readView(edit) {
41
+ const hash = window.location.hash.replace('#', '');
42
+ if (hash === 'topology')
43
+ return 'topology';
44
+ if (hash === 'edit' && edit)
45
+ return 'edit';
46
+ return 'fragments';
47
+ }
48
+ //# sourceMappingURL=console-app.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-app.js","sourceRoot":"","sources":["../../../../../libs/braid-console/src/app/console-app.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createRoot } from 'react-dom/client';
3
+ import { ConsoleApp } from './console-app.js';
4
+ /**
5
+ * Standalone entry — the deployable form.
6
+ *
7
+ * Configuration comes from the DOM rather than a build-time constant, so one bundle serves every
8
+ * environment: the same artifact staging tested is the artifact production runs, pointed
9
+ * elsewhere. Drop a `<script type="application/json" id="braid-console-config">` next to the mount
10
+ * point, or leave it out and the console reads the origin it was served from.
11
+ */
12
+ const mount = document.getElementById('root');
13
+ if (!mount)
14
+ throw new Error('braid-console: no #root element to mount into');
15
+ const config = readConfig();
16
+ // Read-only unless the deployment says otherwise. Editing needs a write API the gateway may not
17
+ // have mounted, and defaulting to an editor that cannot save is a worse first impression than a
18
+ // listing that works — so `edit` adds a tab rather than replacing the console with one.
19
+ createRoot(mount).render(_jsx(ConsoleApp, { api: config, edit: config.edit === true }));
20
+ function readConfig() {
21
+ const element = document.getElementById('braid-console-config');
22
+ if (!element?.textContent)
23
+ return {};
24
+ try {
25
+ return JSON.parse(element.textContent);
26
+ }
27
+ catch {
28
+ console.warn('braid-console: #braid-console-config is not valid JSON; using defaults');
29
+ return {};
30
+ }
31
+ }
32
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../../../../../libs/braid-console/src/app/main.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAG9C;;;;;;;GAOG;AACH,MAAM,KAAK,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;AAC9C,IAAI,CAAC,KAAK;IAAE,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;AAE7E,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;AAE5B,gGAAgG;AAChG,gGAAgG;AAChG,wFAAwF;AACxF,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAC,UAAU,IAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,KAAK,IAAI,GAAI,CAAC,CAAC;AAElF,SAAS,UAAU;IACjB,MAAM,OAAO,GAAG,QAAQ,CAAC,cAAc,CAAC,sBAAsB,CAAC,CAAC;IAChE,IAAI,CAAC,OAAO,EAAE,WAAW;QAAE,OAAO,EAAE,CAAC;IACrC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,CAAoC,CAAC;IAC5E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,IAAI,CAAC,wEAAwE,CAAC,CAAC;QACvF,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
package/src/index.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @braidlabs/console — a read-only view of a Braid gateway's fragment registry.
3
+ *
4
+ * Two ways to use it, from one package:
5
+ *
6
+ * ```tsx
7
+ * // as a library, inside an admin app you already run
8
+ * import { RegistryConsole } from '@braidlabs/console';
9
+ * <RegistryConsole api={{ baseUrl: '/api/gateway' }} />
10
+ * ```
11
+ *
12
+ * ```sh
13
+ * # as a deployable app — a static bundle pointed at a gateway by config
14
+ * nx build-app braid-console
15
+ * ```
16
+ *
17
+ * It reads the discovery endpoint the gateway already serves, so it needs no snapshot store and
18
+ * no write API — it works against a gateway whose manifests are defined in code.
19
+ */
20
+ export { RegistryConsole, filterEntries } from './lib/registry-console.js';
21
+ export { TopologyGraph, layoutTopology } from './lib/topology-graph.js';
22
+ export { IntegrationPanel } from './lib/integration-panel.js';
23
+ export type { IntegrationPanelProps } from './lib/integration-panel.js';
24
+ export { integrationSnippet, allIntegrationSnippets, integrationWarnings, INTEGRATION_TARGETS } from './lib/integration.js';
25
+ export type { IntegrationTarget, IntegrationSnippet } from './lib/integration.js';
26
+ export type { TopologyGraphProps } from './lib/topology-graph.js';
27
+ export { buildTopology, neighborhood, coTenants, edgeKey } from './lib/topology.js';
28
+ export type { Topology, TopologyNode, TopologyEdge, TopologyNodeKind } from './lib/topology.js';
29
+ export type { RegistryConsoleProps } from './lib/registry-console.js';
30
+ export { RegistryEditor } from './lib/registry-editor.js';
31
+ export type { RegistryEditorProps } from './lib/registry-editor.js';
32
+ export { AccessPanel } from './lib/access-panel.js';
33
+ export type { AccessPanelProps } from './lib/access-panel.js';
34
+ export { createDraft, draftStatus, addFragment, removeFragment, updateFragment, resetDraft, parseList, formatList, } from './lib/draft.js';
35
+ export type { Draft, DraftStatus } from './lib/draft.js';
36
+ export { fetchRegistry, fetchHead, listSnapshots, publishSnapshot, pinSnapshot, RegistryFetchError, RegistryApiError, DEFAULT_DISCOVERY_PATH, DEFAULT_API_PATH, } from './lib/client.js';
37
+ export type { ConsoleApi, RegistryListing, HeadState, PublishOutcome } from './lib/client.js';
38
+ export { CONSOLE_STYLES, ensureStyles } from './lib/styles.js';
package/src/index.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @braidlabs/console — a read-only view of a Braid gateway's fragment registry.
3
+ *
4
+ * Two ways to use it, from one package:
5
+ *
6
+ * ```tsx
7
+ * // as a library, inside an admin app you already run
8
+ * import { RegistryConsole } from '@braidlabs/console';
9
+ * <RegistryConsole api={{ baseUrl: '/api/gateway' }} />
10
+ * ```
11
+ *
12
+ * ```sh
13
+ * # as a deployable app — a static bundle pointed at a gateway by config
14
+ * nx build-app braid-console
15
+ * ```
16
+ *
17
+ * It reads the discovery endpoint the gateway already serves, so it needs no snapshot store and
18
+ * no write API — it works against a gateway whose manifests are defined in code.
19
+ */
20
+ export { RegistryConsole, filterEntries } from './lib/registry-console.js';
21
+ export { TopologyGraph, layoutTopology } from './lib/topology-graph.js';
22
+ export { IntegrationPanel } from './lib/integration-panel.js';
23
+ export { integrationSnippet, allIntegrationSnippets, integrationWarnings, INTEGRATION_TARGETS } from './lib/integration.js';
24
+ export { buildTopology, neighborhood, coTenants, edgeKey } from './lib/topology.js';
25
+ export { RegistryEditor } from './lib/registry-editor.js';
26
+ export { AccessPanel } from './lib/access-panel.js';
27
+ export { createDraft, draftStatus, addFragment, removeFragment, updateFragment, resetDraft, parseList, formatList, } from './lib/draft.js';
28
+ export { fetchRegistry, fetchHead, listSnapshots, publishSnapshot, pinSnapshot, RegistryFetchError, RegistryApiError, DEFAULT_DISCOVERY_PATH, DEFAULT_API_PATH, } from './lib/client.js';
29
+ export { CONSOLE_STYLES, ensureStyles } from './lib/styles.js';
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/braid-console/src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACxE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAE9D,OAAO,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAG5H,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAGpF,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAEpD,OAAO,EACL,WAAW,EACX,WAAW,EACX,WAAW,EACX,cAAc,EACd,cAAc,EACd,UAAU,EACV,SAAS,EACT,UAAU,GACX,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,aAAa,EACb,SAAS,EACT,aAAa,EACb,eAAe,EACf,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,gBAAgB,GACjB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import type { FragmentManifest } from '@braidlabs/gateway';
2
+ import { type NamedPrincipal } from '@braidlabs/registry';
3
+ export interface AccessPanelProps {
4
+ /** The draft being edited. */
5
+ manifests: readonly FragmentManifest[];
6
+ /** What is currently pinned, so the panel can show what the draft would change. */
7
+ base?: readonly FragmentManifest[];
8
+ principals: readonly NamedPrincipal[];
9
+ onPrincipalsChange: (principals: NamedPrincipal[]) => void;
10
+ }
11
+ /**
12
+ * Who can see and load what, and what the draft would do to that.
13
+ *
14
+ * The losses lead and the grid follows, because the losses are the finding. A grid on its own asks
15
+ * an operator to scan for the one cell that moved, which is exactly the failure this exists to
16
+ * prevent — a fragment quietly ceasing to be listed for the people who use it.
17
+ *
18
+ * Principals are named by the operator and held here, not persisted: the gateway has no principal
19
+ * directory, and inventing one would make this look authoritative when it is a what-if.
20
+ */
21
+ export declare function AccessPanel({ manifests, base, principals, onPrincipalsChange }: AccessPanelProps): import("react").JSX.Element;
@@ -0,0 +1,35 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import { accessMatrix, ANONYMOUS, parsePrincipal } from '@braidlabs/registry';
4
+ /**
5
+ * Who can see and load what, and what the draft would do to that.
6
+ *
7
+ * The losses lead and the grid follows, because the losses are the finding. A grid on its own asks
8
+ * an operator to scan for the one cell that moved, which is exactly the failure this exists to
9
+ * prevent — a fragment quietly ceasing to be listed for the people who use it.
10
+ *
11
+ * Principals are named by the operator and held here, not persisted: the gateway has no principal
12
+ * directory, and inventing one would make this look authoritative when it is a what-if.
13
+ */
14
+ export function AccessPanel({ manifests, base, principals, onPrincipalsChange }) {
15
+ const [draftPrincipal, setDraftPrincipal] = useState('');
16
+ const matrix = useMemo(() => accessMatrix(manifests, [ANONYMOUS, ...principals], base), [manifests, principals, base]);
17
+ const addPrincipal = () => {
18
+ const spec = draftPrincipal.trim();
19
+ if (!spec)
20
+ return;
21
+ const parsed = parsePrincipal(spec);
22
+ if (parsed.name === ANONYMOUS.name)
23
+ return; // always present; adding it again would duplicate a column
24
+ onPrincipalsChange([...principals.filter((p) => p.name !== parsed.name), parsed]);
25
+ setDraftPrincipal('');
26
+ };
27
+ return (_jsxs("section", { className: "braid-console__access", children: [_jsxs("div", { className: "braid-console__accesshead", children: [_jsx("strong", { children: "Access" }), _jsx("span", { className: "braid-console__desc", children: "Named principals, not real users \u2014 the gateway holds no directory." }), _jsx("span", { className: "braid-console__spacer" }), _jsx("input", { className: "braid-console__search", value: draftPrincipal, placeholder: "trader:roles=trader", "aria-label": "Add a principal to test as", onChange: (event) => setDraftPrincipal(event.target.value), onKeyDown: (event) => event.key === 'Enter' && addPrincipal() }), _jsx("button", { className: "braid-console__ghost", type: "button", onClick: addPrincipal, children: "Test as" })] }), base && matrix.unchanged && _jsx("p", { className: "braid-console__desc", children: "This draft changes nobody\u2019s access." }), matrix.losses.length > 0 && (_jsx("ul", { className: "braid-console__findings", role: "alert", children: matrix.losses.map((loss, index) => (_jsxs("li", { className: "braid-console__finding braid-console__finding--error", children: [_jsx("strong", { children: loss.principal }), " can no longer ", loss.action, " ", _jsx("strong", { children: loss.fragmentId }), loss.to === 'absent' && _jsx("span", { className: "braid-console__desc", children: " (fragment removed)" })] }, index))) })), matrix.gains.length > 0 && (_jsx("ul", { className: "braid-console__findings", children: matrix.gains.map((gain, index) => (_jsxs("li", { className: "braid-console__finding", children: [_jsx("strong", { children: gain.principal }), " can now ", gain.action, " ", _jsx("strong", { children: gain.fragmentId })] }, index))) })), _jsxs("table", { className: "braid-console__table braid-console__matrix", children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { scope: "col", children: "Fragment" }), matrix.principals.map((name) => (_jsxs("th", { scope: "col", children: [name, name !== ANONYMOUS.name && (_jsx("button", { className: "braid-console__ghost braid-console__remove", type: "button", "aria-label": `Stop testing as ${name}`, onClick: () => onPrincipalsChange(principals.filter((p) => p.name !== name)), children: "\u00D7" }))] }, name)))] }) }), _jsx("tbody", { children: matrix.rows.map((row) => (_jsxs("tr", { children: [_jsxs("td", { children: [_jsx("span", { className: "braid-console__id", children: row.fragmentId }), ' ', _jsx("span", { className: "braid-console__mono", children: row.action })] }), row.cells.map((cellValue) => (_jsx("td", { children: cellValue.changed && base ? (_jsxs(_Fragment, { children: [_jsx(Mark, { outcome: cellValue.before }), " ", _jsx("span", { "aria-hidden": "true", children: "\u2192" }), ' ', _jsx(Mark, { outcome: cellValue.after })] })) : (_jsx(Mark, { outcome: cellValue.after })) }, cellValue.principal)))] }, `${row.fragmentId}:${row.action}`))) })] })] }));
28
+ }
29
+ /** Never colour alone — the label is what a screen reader and a colourblind reader both get. */
30
+ function Mark({ outcome }) {
31
+ const symbol = outcome === 'allowed' ? '✓' : outcome === 'denied' ? '✗' : '·';
32
+ const label = outcome === 'allowed' ? 'allowed' : outcome === 'denied' ? 'denied' : 'not present';
33
+ return (_jsxs("span", { className: `braid-console__mark braid-console__mark--${outcome}`, title: label, children: [_jsx("span", { "aria-hidden": "true", children: symbol }), _jsx("span", { className: "braid-console__sronly", children: label })] }));
34
+ }
35
+ //# sourceMappingURL=access-panel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-panel.js","sourceRoot":"","sources":["../../../../../libs/braid-console/src/lib/access-panel.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import type { DiscoveryEntry, FragmentManifest } from '@braidlabs/gateway';
2
+ import type { DescriptorNote, RegistryDiff, RegistryFinding, SnapshotRef } from '@braidlabs/registry';
3
+ /**
4
+ * How the console reaches a gateway.
5
+ *
6
+ * Identity belongs to the host, exactly as it does for the gateway's own `principal` resolver:
7
+ * this takes a base URL and a headers hook (or a whole `fetch`) and never performs a login. A
8
+ * console mounted inside an internal admin app should inherit that app's session, not open a
9
+ * second one.
10
+ */
11
+ export interface ConsoleApi {
12
+ /** Where the gateway is. Defaults to the page's own origin. */
13
+ baseUrl?: string;
14
+ /** Path of the discovery listing. Defaults to `/__braid/registry`. */
15
+ discoveryPath?: string;
16
+ /**
17
+ * Path the registry write API is mounted at. Defaults to `/__braid/registry-api`.
18
+ *
19
+ * Editing needs this API; the read-only console does not. A gateway that has not mounted one is
20
+ * simply not editable, which is the common case and not an error.
21
+ */
22
+ apiPath?: string;
23
+ /** Called before every request, so a short-lived token can be refreshed by the host. */
24
+ headers?: () => Record<string, string> | Promise<Record<string, string>>;
25
+ /** Full override, for hosts with their own fetch wrapper (retries, tracing, mTLS). */
26
+ fetch?: typeof fetch;
27
+ }
28
+ export declare const DEFAULT_DISCOVERY_PATH = "/__braid/registry";
29
+ export interface RegistryListing {
30
+ entries: DiscoveryEntry[];
31
+ total: number;
32
+ /** True when the gateway is in development mode and skipped access filtering. */
33
+ unfiltered: boolean;
34
+ protocolVersion: string;
35
+ }
36
+ /**
37
+ * Reads a gateway's registry listing, following pagination to the end.
38
+ *
39
+ * The listing is what the *caller* may see — the gateway filters by `access.list` — so a console
40
+ * showing fewer fragments than the operator expects is usually correct and worth surfacing rather
41
+ * than hiding. {@link RegistryListing.unfiltered} distinguishes "you are seeing everything because
42
+ * this gateway is in development" from "you are seeing what you are allowed to see".
43
+ */
44
+ export declare function fetchRegistry(api?: ConsoleApi, signal?: AbortSignal): Promise<RegistryListing>;
45
+ export declare class RegistryFetchError extends Error {
46
+ readonly url: string;
47
+ readonly status: number;
48
+ constructor(url: string, status: number);
49
+ }
50
+ export declare const DEFAULT_API_PATH = "/__braid/registry-api";
51
+ export interface HeadState {
52
+ id: string | null;
53
+ snapshot: {
54
+ id: string;
55
+ createdAt: string;
56
+ manifests: FragmentManifest[];
57
+ } | null;
58
+ }
59
+ export interface PublishOutcome {
60
+ snapshot: {
61
+ id: string;
62
+ createdAt: string;
63
+ fragmentCount: number;
64
+ };
65
+ findings: RegistryFinding[];
66
+ descriptorNotes: DescriptorNote[];
67
+ pinned: boolean;
68
+ diff?: RegistryDiff;
69
+ }
70
+ /** The currently pinned snapshot, or `{ id: null }` when nothing has been published. */
71
+ export declare function fetchHead(api?: ConsoleApi, signal?: AbortSignal): Promise<HeadState>;
72
+ export declare function listSnapshots(api?: ConsoleApi, signal?: AbortSignal): Promise<{
73
+ items: SnapshotRef[];
74
+ head: string | null;
75
+ }>;
76
+ /**
77
+ * Publishes a draft.
78
+ *
79
+ * The server re-validates whatever arrives, so a 422 here is not a client bug — it is the
80
+ * authoritative check disagreeing with the optimistic one, and its findings are what to show.
81
+ */
82
+ export declare function publishSnapshot(api: ConsoleApi, body: {
83
+ manifests: FragmentManifest[];
84
+ labels?: Record<string, string>;
85
+ pin?: boolean;
86
+ }): Promise<PublishOutcome>;
87
+ /** Re-points HEAD. Rollback is this and nothing else. */
88
+ export declare function pinSnapshot(api: ConsoleApi, id: string): Promise<void>;
89
+ export declare class RegistryApiError extends Error {
90
+ readonly status: number;
91
+ readonly findings: RegistryFinding[];
92
+ readonly descriptorNotes: DescriptorNote[];
93
+ constructor(message: string, status: number, findings?: RegistryFinding[], descriptorNotes?: DescriptorNote[]);
94
+ }
@@ -0,0 +1,109 @@
1
+ export const DEFAULT_DISCOVERY_PATH = '/__braid/registry';
2
+ /**
3
+ * Reads a gateway's registry listing, following pagination to the end.
4
+ *
5
+ * The listing is what the *caller* may see — the gateway filters by `access.list` — so a console
6
+ * showing fewer fragments than the operator expects is usually correct and worth surfacing rather
7
+ * than hiding. {@link RegistryListing.unfiltered} distinguishes "you are seeing everything because
8
+ * this gateway is in development" from "you are seeing what you are allowed to see".
9
+ */
10
+ export async function fetchRegistry(api = {}, signal) {
11
+ const doFetch = api.fetch ?? globalThis.fetch;
12
+ const base = api.baseUrl?.replace(/\/$/, '') ?? '';
13
+ const path = api.discoveryPath ?? DEFAULT_DISCOVERY_PATH;
14
+ const entries = [];
15
+ let page = 1;
16
+ let last;
17
+ do {
18
+ const url = `${base}${path}?page=${page}`;
19
+ const response = await doFetch(url, {
20
+ headers: { accept: 'application/json', ...(await resolveHeaders(api)) },
21
+ ...(signal ? { signal } : {}),
22
+ });
23
+ if (!response.ok)
24
+ throw new RegistryFetchError(url, response.status);
25
+ last = (await response.json());
26
+ entries.push(...last.items);
27
+ page += 1;
28
+ // guard against a gateway that reports hasMore forever
29
+ } while (last.hasMore && page <= last.totalPages && page < 1000);
30
+ return {
31
+ entries,
32
+ total: last?.total ?? entries.length,
33
+ unfiltered: last?.unfiltered === true,
34
+ protocolVersion: last?.protocolVersion ?? 'unknown',
35
+ };
36
+ }
37
+ export class RegistryFetchError extends Error {
38
+ url;
39
+ status;
40
+ constructor(url, status) {
41
+ super(status === 404
42
+ ? `No registry at ${url}. Discovery is off unless the gateway is configured with it — pass \`discovery\` to createGateway.`
43
+ : status === 401 || status === 403
44
+ ? `Not authorized to list the registry at ${url}.`
45
+ : `Reading the registry at ${url} failed with HTTP ${status}.`);
46
+ this.url = url;
47
+ this.status = status;
48
+ this.name = 'RegistryFetchError';
49
+ }
50
+ }
51
+ async function resolveHeaders(api) {
52
+ return api.headers ? await api.headers() : {};
53
+ }
54
+ // ---------------------------------------------------------------------------
55
+ // Write API — only needed for editing
56
+ // ---------------------------------------------------------------------------
57
+ export const DEFAULT_API_PATH = '/__braid/registry-api';
58
+ /** The currently pinned snapshot, or `{ id: null }` when nothing has been published. */
59
+ export async function fetchHead(api = {}, signal) {
60
+ return apiRequest(api, '/head', { method: 'GET' }, signal);
61
+ }
62
+ export async function listSnapshots(api = {}, signal) {
63
+ return apiRequest(api, '/snapshots', { method: 'GET' }, signal);
64
+ }
65
+ /**
66
+ * Publishes a draft.
67
+ *
68
+ * The server re-validates whatever arrives, so a 422 here is not a client bug — it is the
69
+ * authoritative check disagreeing with the optimistic one, and its findings are what to show.
70
+ */
71
+ export async function publishSnapshot(api, body) {
72
+ return apiRequest(api, '/snapshots', { method: 'POST', body: JSON.stringify(body) });
73
+ }
74
+ /** Re-points HEAD. Rollback is this and nothing else. */
75
+ export async function pinSnapshot(api, id) {
76
+ await apiRequest(api, '/head', { method: 'POST', body: JSON.stringify({ id }) });
77
+ }
78
+ export class RegistryApiError extends Error {
79
+ status;
80
+ findings;
81
+ descriptorNotes;
82
+ constructor(message, status, findings = [], descriptorNotes = []) {
83
+ super(message);
84
+ this.status = status;
85
+ this.findings = findings;
86
+ this.descriptorNotes = descriptorNotes;
87
+ this.name = 'RegistryApiError';
88
+ }
89
+ }
90
+ async function apiRequest(api, route, init, signal) {
91
+ const doFetch = api.fetch ?? globalThis.fetch;
92
+ const base = api.baseUrl?.replace(/\/$/, '') ?? '';
93
+ const url = `${base}${api.apiPath ?? DEFAULT_API_PATH}${route}`;
94
+ const response = await doFetch(url, {
95
+ ...init,
96
+ headers: {
97
+ accept: 'application/json',
98
+ ...(init.body ? { 'content-type': 'application/json' } : {}),
99
+ ...(await resolveHeaders(api)),
100
+ },
101
+ ...(signal ? { signal } : {}),
102
+ });
103
+ const body = (await response.json().catch(() => ({})));
104
+ if (!response.ok) {
105
+ throw new RegistryApiError(typeof body.error === 'string' ? body.error : `HTTP ${response.status} from ${url}`, response.status, body.findings ?? [], body.descriptorNotes ?? []);
106
+ }
107
+ return body;
108
+ }
109
+ //# sourceMappingURL=client.js.map