@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.
- package/README.md +131 -0
- package/package.json +43 -0
- package/src/app/console-app.d.ts +17 -0
- package/src/app/console-app.js +48 -0
- package/src/app/console-app.js.map +1 -0
- package/src/app/main.d.ts +1 -0
- package/src/app/main.js +32 -0
- package/src/app/main.js.map +1 -0
- package/src/index.d.ts +38 -0
- package/src/index.js +30 -0
- package/src/index.js.map +1 -0
- package/src/lib/access-panel.d.ts +21 -0
- package/src/lib/access-panel.js +35 -0
- package/src/lib/access-panel.js.map +1 -0
- package/src/lib/client.d.ts +94 -0
- package/src/lib/client.js +109 -0
- package/src/lib/client.js.map +1 -0
- package/src/lib/draft.d.ts +45 -0
- package/src/lib/draft.js +85 -0
- package/src/lib/draft.js.map +1 -0
- package/src/lib/integration-panel.d.ts +13 -0
- package/src/lib/integration-panel.js +85 -0
- package/src/lib/integration-panel.js.map +1 -0
- package/src/lib/integration.d.ts +36 -0
- package/src/lib/integration.js +148 -0
- package/src/lib/integration.js.map +1 -0
- package/src/lib/registry-console.d.ts +46 -0
- package/src/lib/registry-console.js +88 -0
- package/src/lib/registry-console.js.map +1 -0
- package/src/lib/registry-editor.d.ts +26 -0
- package/src/lib/registry-editor.js +133 -0
- package/src/lib/registry-editor.js.map +1 -0
- package/src/lib/styles.d.ts +27 -0
- package/src/lib/styles.js +470 -0
- package/src/lib/styles.js.map +1 -0
- package/src/lib/topology-graph.d.ts +47 -0
- package/src/lib/topology-graph.js +124 -0
- package/src/lib/topology-graph.js.map +1 -0
- package/src/lib/topology.d.ts +86 -0
- package/src/lib/topology.js +122 -0
- 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 {};
|
package/src/app/main.js
ADDED
|
@@ -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
|
package/src/index.js.map
ADDED
|
@@ -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":";AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAE1C,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,cAAc,EAAuB,MAAM,qBAAqB,CAAC;AAWnG;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CAAC,EAAE,SAAS,EAAE,IAAI,EAAE,UAAU,EAAE,kBAAkB,EAAoB;IAC/F,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAC;IAEzD,MAAM,MAAM,GAAG,OAAO,CACpB,GAAG,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,CAAC,SAAS,EAAE,GAAG,UAAU,CAAC,EAAE,IAAI,CAAC,EAC/D,CAAC,SAAS,EAAE,UAAU,EAAE,IAAI,CAAC,CAC9B,CAAC;IAEF,MAAM,YAAY,GAAG,GAAG,EAAE;QACxB,MAAM,IAAI,GAAG,cAAc,CAAC,IAAI,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,OAAO;QAClB,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACpC,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI;YAAE,OAAO,CAAC,2DAA2D;QACvG,kBAAkB,CAAC,CAAC,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAClF,iBAAiB,CAAC,EAAE,CAAC,CAAC;IACxB,CAAC,CAAC;IAEF,OAAO,CACL,mBAAS,SAAS,EAAC,uBAAuB,aACxC,eAAK,SAAS,EAAC,2BAA2B,aACxC,sCAAuB,EACvB,eAAM,SAAS,EAAC,qBAAqB,wFAE9B,EACP,eAAM,SAAS,EAAC,uBAAuB,GAAG,EAC1C,gBACE,SAAS,EAAC,uBAAuB,EACjC,KAAK,EAAE,cAAc,EACrB,WAAW,EAAC,qBAAqB,gBACtB,4BAA4B,EACvC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAC1D,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,KAAK,OAAO,IAAI,YAAY,EAAE,GAC7D,EACF,iBAAQ,SAAS,EAAC,sBAAsB,EAAC,IAAI,EAAC,QAAQ,EAAC,OAAO,EAAE,YAAY,wBAEnE,IACL,EAEL,IAAI,IAAI,MAAM,CAAC,SAAS,IAAI,YAAG,SAAS,EAAC,qBAAqB,yDAAwC,EAEtG,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,CAC3B,aAAI,SAAS,EAAC,yBAAyB,EAAC,IAAI,EAAC,OAAO,YACjD,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CAClC,cAAgB,SAAS,EAAC,sDAAsD,aAC9E,2BAAS,IAAI,CAAC,SAAS,GAAU,qBAAgB,IAAI,CAAC,MAAM,OAAE,2BAAS,IAAI,CAAC,UAAU,GAAU,EAC/F,IAAI,CAAC,EAAE,KAAK,QAAQ,IAAI,eAAM,SAAS,EAAC,qBAAqB,oCAA2B,KAFlF,KAAK,CAGT,CACN,CAAC,GACC,CACN,EAEA,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAC1B,aAAI,SAAS,EAAC,yBAAyB,YACpC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,CACjC,cAAgB,SAAS,EAAC,wBAAwB,aAChD,2BAAS,IAAI,CAAC,SAAS,GAAU,eAAU,IAAI,CAAC,MAAM,OAAE,2BAAS,IAAI,CAAC,UAAU,GAAU,KADnF,KAAK,CAET,CACN,CAAC,GACC,CACN,EAED,iBAAO,SAAS,EAAC,4CAA4C,aAC3D,0BACE,yBACE,aAAI,KAAK,EAAC,KAAK,yBAAc,EAC5B,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAC/B,cAAI,KAAK,EAAC,KAAK,aACZ,IAAI,EACJ,IAAI,KAAK,SAAS,CAAC,IAAI,IAAI,CAC1B,iBACE,SAAS,EAAC,4CAA4C,EACtD,IAAI,EAAC,QAAQ,gBACD,mBAAmB,IAAI,EAAE,EACrC,OAAO,EAAE,GAAG,EAAE,CAAC,kBAAkB,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,uBAGrE,CACV,KAXkB,IAAI,CAYpB,CACN,CAAC,IACC,GACC,EACR,0BACG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CACxB,yBACE,yBACE,eAAM,SAAS,EAAC,mBAAmB,YAAE,GAAG,CAAC,UAAU,GAAQ,EAAC,GAAG,EAC/D,eAAM,SAAS,EAAC,qBAAqB,YAAE,GAAG,CAAC,MAAM,GAAQ,IACtD,EACJ,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,CAC5B,uBACG,SAAS,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,CAC3B,8BACE,KAAC,IAAI,IAAC,OAAO,EAAE,SAAS,CAAC,MAAM,GAAI,OAAC,8BAAkB,MAAM,uBAAS,EAAC,GAAG,EACzE,KAAC,IAAI,IAAC,OAAO,EAAE,SAAS,CAAC,KAAK,GAAI,IACjC,CACJ,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,OAAO,EAAE,SAAS,CAAC,KAAK,GAAI,CACnC,IARM,SAAS,CAAC,SAAS,CASvB,CACN,CAAC,KAhBK,GAAG,GAAG,CAAC,UAAU,IAAI,GAAG,CAAC,MAAM,EAAE,CAiBrC,CACN,CAAC,GACI,IACF,IACA,CACX,CAAC;AACJ,CAAC;AAED,gGAAgG;AAChG,SAAS,IAAI,CAAC,EAAE,OAAO,EAAgD;IACrE,MAAM,MAAM,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;IAC9E,MAAM,KAAK,GAAG,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC;IAElG,OAAO,CACL,gBAAM,SAAS,EAAE,4CAA4C,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,aAClF,8BAAkB,MAAM,YAAE,MAAM,GAAQ,EACxC,eAAM,SAAS,EAAC,uBAAuB,YAAE,KAAK,GAAQ,IACjD,CACR,CAAC;AACJ,CAAC"}
|
|
@@ -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
|