@manta-eu/horns-client 0.1.0 → 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 (71) hide show
  1. package/component-api.d.ts +7 -0
  2. package/dist/app/coverage-run.js +4 -3
  3. package/dist/app/home-page.js +6 -4
  4. package/dist/app/horn-page.js +7 -10
  5. package/dist/app/horn-studio.js +63 -10
  6. package/dist/app/horn-view-page.js +13 -9
  7. package/dist/app/horns-page.js +6 -9
  8. package/dist/app/new-horn-page.js +52 -10
  9. package/dist/app/paths.d.ts +15 -10
  10. package/dist/app/paths.js +18 -29
  11. package/dist/app/plans-page.d.ts +1 -0
  12. package/dist/app/plans-page.js +59 -0
  13. package/dist/app/reference-page.d.ts +1 -0
  14. package/dist/app/reference-page.js +55 -0
  15. package/dist/app/routes.js +7 -5
  16. package/dist/app/rule-page.js +18 -11
  17. package/dist/app/rules-page.js +12 -12
  18. package/dist/app/shape-entity-page.js +7 -9
  19. package/dist/app/shape-zoo-page.js +9 -10
  20. package/dist/app/shapes-page.js +13 -15
  21. package/dist/app/showcase-page.js +8 -11
  22. package/dist/components/app-shell.js +8 -7
  23. package/dist/components/catalog-card-view.js +4 -5
  24. package/dist/components/explain-dialog.d.ts +5 -0
  25. package/dist/components/explain-dialog.js +26 -0
  26. package/dist/components/plan-runs.js +2 -2
  27. package/dist/components/read-the-code.d.ts +19 -0
  28. package/dist/components/read-the-code.js +17 -3
  29. package/dist/index.d.ts +2 -3
  30. package/dist/index.js +2 -3
  31. package/dist/lib/horn-review.d.ts +7 -0
  32. package/dist/lib/horn-review.js +15 -0
  33. package/dist/lib/horn-variables.d.ts +8 -0
  34. package/dist/lib/horn-variables.js +39 -0
  35. package/dist/lib/manta/sign-in.d.ts +2 -0
  36. package/dist/lib/manta/sign-in.js +9 -0
  37. package/dist/lib/manta/sparql-select.d.ts +6 -2
  38. package/dist/lib/manta/sparql-select.js +17 -14
  39. package/dist/lib/reference/fake-sparql.d.ts +4 -0
  40. package/dist/lib/reference/fake-sparql.js +216 -0
  41. package/dist/lib/shapes/ldkit-source.d.ts +1 -1
  42. package/dist/lib/shapes/ldkit-source.js +1 -1
  43. package/dist/lib/studio-progress.js +3 -1
  44. package/dist/lib/surface/declarations.d.ts +5 -0
  45. package/dist/lib/surface/declarations.js +77 -0
  46. package/dist/lib/written-surface.js +13 -2
  47. package/dist/standalone/app.d.ts +2 -0
  48. package/dist/standalone/app.js +57 -0
  49. package/dist/standalone/client.d.ts +15 -0
  50. package/dist/{client.js → standalone/client.js} +12 -8
  51. package/dist/standalone/gates.d.ts +10 -0
  52. package/dist/standalone/gates.js +15 -0
  53. package/dist/styles.css +1 -1
  54. package/dist/vite/component-api.d.ts +8 -0
  55. package/dist/vite/component-api.js +24 -0
  56. package/dist/vite/index.d.ts +3 -0
  57. package/dist/vite/index.js +3 -0
  58. package/dist/vite/shape-catalog.d.ts +10 -0
  59. package/dist/vite/shape-catalog.js +35 -0
  60. package/dist/vite/typecheck-fs.d.ts +6 -0
  61. package/dist/vite/typecheck-fs.js +68 -0
  62. package/package.json +36 -12
  63. package/shape-catalog.d.ts +9 -0
  64. package/typecheck-fs.d.ts +6 -0
  65. package/dist/app/session-gate.d.ts +0 -6
  66. package/dist/app/session-gate.js +0 -19
  67. package/dist/client.d.ts +0 -10
  68. package/dist/lib/manta/providers.d.ts +0 -10
  69. package/dist/lib/manta/providers.js +0 -48
  70. package/dist/lib/manta/session.d.ts +0 -47
  71. package/dist/lib/manta/session.js +0 -204
@@ -0,0 +1,8 @@
1
+ import type { Plugin } from "vite";
2
+ export declare const componentApiModuleId = "virtual:horns/component-api";
3
+ /**
4
+ * What each surfaced component takes, as `virtual:horns/component-api`, read from `@manta-eu/react`'s `.d.ts`.
5
+ *
6
+ * A worker has no filesystem, so the declarations arrive as data, the way the shape catalog does.
7
+ */
8
+ export declare function hornsComponentApi(): Plugin;
@@ -0,0 +1,24 @@
1
+ import { componentApiFrom, sdkDistRoot } from "../lib/surface/declarations.js";
2
+ export const componentApiModuleId = "virtual:horns/component-api";
3
+ /**
4
+ * What each surfaced component takes, as `virtual:horns/component-api`, read from `@manta-eu/react`'s `.d.ts`.
5
+ *
6
+ * A worker has no filesystem, so the declarations arrive as data, the way the shape catalog does.
7
+ */
8
+ export function hornsComponentApi() {
9
+ const resolved = `\0${componentApiModuleId}`;
10
+ let built;
11
+ return {
12
+ name: "horns-component-api",
13
+ resolveId: (id) => (id === componentApiModuleId ? resolved : undefined),
14
+ load(id) {
15
+ if (id !== resolved)
16
+ return undefined;
17
+ if (built !== undefined)
18
+ return built;
19
+ const api = componentApiFrom(sdkDistRoot());
20
+ built = `export const api = ${JSON.stringify(api)};\n`;
21
+ return built;
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,3 @@
1
+ export { hornsShapeCatalog, shapeCatalogModuleId } from "./shape-catalog.ts";
2
+ export { hornsComponentApi, componentApiModuleId } from "./component-api.ts";
3
+ export { hornsTypecheckFs, typecheckFsModuleId } from "./typecheck-fs.ts";
@@ -0,0 +1,3 @@
1
+ export { hornsShapeCatalog, shapeCatalogModuleId } from "./shape-catalog.js";
2
+ export { hornsComponentApi, componentApiModuleId } from "./component-api.js";
3
+ export { hornsTypecheckFs, typecheckFsModuleId } from "./typecheck-fs.js";
@@ -0,0 +1,10 @@
1
+ import type { Plugin } from "vite";
2
+ import { type Realm } from "@manta-eu/horns";
3
+ export declare const shapeCatalogModuleId = "virtual:horns/shape-catalog";
4
+ /**
5
+ * The realm's shape catalog as `virtual:horns/shape-catalog`, built from `@manta-eu/ldo` at build time.
6
+ *
7
+ * A worker cannot import the SDK's schema entry without stopping workerd, so the catalog has to arrive as
8
+ * data. One realm per build, because the SDK resolves its realm at module load.
9
+ */
10
+ export declare function hornsShapeCatalog(realm: Realm): Plugin;
@@ -0,0 +1,35 @@
1
+ import { realmEnv } from "@manta-eu/horns";
2
+ import { shapeCatalogFromLdkit } from "@manta-eu/horns/shapes";
3
+ import { ldkitSchemasFor } from "../lib/shapes/ldkit-source.js";
4
+ export const shapeCatalogModuleId = "virtual:horns/shape-catalog";
5
+ // Sorted, so the emitted chunk changes when the shapes do and not when the SDK reorders its exports.
6
+ function stable(catalog) {
7
+ const compare = (a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b));
8
+ return { shapes: [...catalog.shapes].sort(compare), untargeted: [...catalog.untargeted].sort() };
9
+ }
10
+ /**
11
+ * The realm's shape catalog as `virtual:horns/shape-catalog`, built from `@manta-eu/ldo` at build time.
12
+ *
13
+ * A worker cannot import the SDK's schema entry without stopping workerd, so the catalog has to arrive as
14
+ * data. One realm per build, because the SDK resolves its realm at module load.
15
+ */
16
+ export function hornsShapeCatalog(realm) {
17
+ const resolved = `\0${shapeCatalogModuleId}`;
18
+ let built;
19
+ return {
20
+ name: "horns-shape-catalog",
21
+ resolveId: (id) => (id === shapeCatalogModuleId ? resolved : undefined),
22
+ async load(id) {
23
+ if (id !== resolved)
24
+ return undefined;
25
+ if (built !== undefined)
26
+ return built;
27
+ // Before the import, not after: the schemas read the realm as they load.
28
+ Object.assign(process.env, realmEnv(realm));
29
+ const { module } = await ldkitSchemasFor(realm);
30
+ const catalog = stable(shapeCatalogFromLdkit(module, realm));
31
+ built = `export const catalog = ${JSON.stringify(catalog)};\n`;
32
+ return built;
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from "vite";
2
+ export declare const typecheckFsModuleId = "virtual:horns/typecheck-fs";
3
+ /** Node only: it reads the filesystem. The plugin calls it at build time; a worker never does. */
4
+ export declare function typecheckFsFrom(): Record<string, string>;
5
+ /** `virtual:horns/typecheck-fs`: TypeScript's own lib files plus the typings a view may import. */
6
+ export declare function hornsTypecheckFs(): Plugin;
@@ -0,0 +1,68 @@
1
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
2
+ import { dirname, join, relative } from "node:path";
3
+ import { createRequire } from "node:module";
4
+ export const typecheckFsModuleId = "virtual:horns/typecheck-fs";
5
+ // The declarations a written view is checked against, gathered at build time because a worker has no
6
+ // filesystem. Only `.d.ts` and the manifests the resolver reads: no implementation ships.
7
+ const typed = ["@manta-eu/react", "@types/react", "csstype", "zod"];
8
+ // The kit's own surface module. It is a workspace package whose `exports` point at `.tsx`, so what the
9
+ // resolver wants is the source, not declarations — there are none to collect in this repo.
10
+ const typedFromSource = ["@manta-eu/horns-kit"];
11
+ function declarationsOf(pkg, root, into, fromSource = false) {
12
+ const extensions = fromSource ? [".ts", ".tsx"] : [".d.ts"];
13
+ const walk = (directory) => {
14
+ for (const entry of readdirSync(directory)) {
15
+ const path = join(directory, entry);
16
+ if (statSync(path).isDirectory()) {
17
+ // A workspace package is a link to its own checkout, so its `node_modules` and a built `dist` sit
18
+ // under it: following either pulls the tree in twice over, and `exports` points at `src` anyway.
19
+ // A published package is the other way round — `dist` is exactly where its typings are.
20
+ if (entry === "node_modules" || (fromSource && entry === "dist"))
21
+ continue;
22
+ walk(path);
23
+ continue;
24
+ }
25
+ if (!extensions.some((one) => path.endsWith(one)) && entry !== "package.json")
26
+ continue;
27
+ into.set(`/node_modules/${pkg}/${relative(root, path).split("\\").join("/")}`, readFileSync(path, "utf8"));
28
+ }
29
+ };
30
+ walk(root);
31
+ }
32
+ /** Node only: it reads the filesystem. The plugin calls it at build time; a worker never does. */
33
+ export function typecheckFsFrom() {
34
+ const require = createRequire(import.meta.url);
35
+ const files = new Map();
36
+ const lib = dirname(require.resolve("typescript"));
37
+ for (const entry of readdirSync(lib)) {
38
+ if (/^lib\..*\.d\.ts$/.test(entry))
39
+ files.set(`/${entry}`, readFileSync(join(lib, entry), "utf8"));
40
+ }
41
+ for (const pkg of [...typed, ...typedFromSource]) {
42
+ // Found by path, not by resolving `${pkg}/package.json`: an `exports` map may not list the manifest
43
+ // (`@manta-eu/react` does not), and a types-only package has no entry to resolve at all.
44
+ const root = (require.resolve.paths(pkg) ?? [])
45
+ .map((base) => join(base, pkg))
46
+ .find((candidate) => existsSync(join(candidate, "package.json")));
47
+ if (root === undefined)
48
+ throw new Error(`No typings found for ${pkg}; a written view cannot be checked.`);
49
+ declarationsOf(pkg, root, files, typedFromSource.includes(pkg));
50
+ }
51
+ // Sorted, so the chunk changes when the typings do and not when a directory is read back in another order.
52
+ return Object.fromEntries([...files].sort(([a], [b]) => a.localeCompare(b)));
53
+ }
54
+ /** `virtual:horns/typecheck-fs`: TypeScript's own lib files plus the typings a view may import. */
55
+ export function hornsTypecheckFs() {
56
+ const resolved = `\0${typecheckFsModuleId}`;
57
+ let built;
58
+ return {
59
+ name: "horns-typecheck-fs",
60
+ resolveId: (id) => (id === typecheckFsModuleId ? resolved : undefined),
61
+ load(id) {
62
+ if (id !== resolved)
63
+ return undefined;
64
+ built ??= `export const fs = ${JSON.stringify(typecheckFsFrom())};\n`;
65
+ return built;
66
+ },
67
+ };
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manta-eu/horns-client",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "The Manta Horns app as a library: routes, providers and the written-view runtime for a React host.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,7 +11,10 @@
11
11
  "type": "module",
12
12
  "files": [
13
13
  "dist",
14
- "LICENSE"
14
+ "LICENSE",
15
+ "shape-catalog.d.ts",
16
+ "component-api.d.ts",
17
+ "typecheck-fs.d.ts"
15
18
  ],
16
19
  "exports": {
17
20
  ".": {
@@ -24,12 +27,27 @@
24
27
  },
25
28
  "./styles.css": "./dist/styles.css",
26
29
  "./mount": {
27
- "types": "./dist/client.d.ts",
28
- "default": "./dist/client.js"
30
+ "types": "./dist/standalone/client.d.ts",
31
+ "default": "./dist/standalone/client.js"
29
32
  },
30
33
  "./ldkit-source": {
31
34
  "types": "./dist/lib/shapes/ldkit-source.d.ts",
32
35
  "default": "./dist/lib/shapes/ldkit-source.js"
36
+ },
37
+ "./vite": {
38
+ "types": "./dist/vite/index.d.ts",
39
+ "default": "./dist/vite/index.js"
40
+ },
41
+ "./shape-catalog": "./shape-catalog.d.ts",
42
+ "./component-api": "./component-api.d.ts",
43
+ "./surface-declarations": {
44
+ "types": "./dist/lib/surface/declarations.d.ts",
45
+ "default": "./dist/lib/surface/declarations.js"
46
+ },
47
+ "./typecheck-fs": "./typecheck-fs.d.ts",
48
+ "./typecheck-fs-source": {
49
+ "types": "./dist/vite/typecheck-fs.d.ts",
50
+ "default": "./dist/vite/typecheck-fs.js"
33
51
  }
34
52
  },
35
53
  "publishConfig": {
@@ -38,25 +56,31 @@
38
56
  },
39
57
  "dependencies": {
40
58
  "@ai-sdk/react": "^3.0.237",
41
- "@manta-eu/ldo": "0.1.4",
42
- "@manta-eu/lib": "0.1.4",
43
- "@manta-eu/rdf-lib": "0.1.4",
44
- "@manta-eu/react": "0.1.4",
59
+ "@manta-eu/client": "0.1.5",
60
+ "@manta-eu/ldo": "0.1.5",
61
+ "@manta-eu/lib": "0.1.5",
62
+ "@manta-eu/rdf-lib": "0.1.5",
63
+ "@manta-eu/react": "0.1.5",
45
64
  "@posthog/react": "^1.10.3",
46
65
  "@sentry/react": "^10.70.0",
47
66
  "@tanstack/react-query": "^5.101.4",
67
+ "@ulb-darmstadt/shacl-form": "^2.1.1-rc1",
68
+ "@xyflow/react": "^12.4.4",
69
+ "graphql-request": "^7.3.0",
48
70
  "html-to-image": "1.11.13",
49
- "ldkit": "2.8.0",
71
+ "ldkit": "2.9.0",
50
72
  "lucide-react": "^0.525.0",
73
+ "next-themes": "^0.4.6",
74
+ "nuqs": "^2.10.1",
51
75
  "posthog-js": "^1.418.0",
52
76
  "react": "19.2.4",
53
77
  "react-dom": "19.2.4",
54
78
  "react-router": "^7.9.6",
55
79
  "ts-pattern": "^5.9.0",
56
80
  "zod": "^4.5.4",
57
- "@manta-eu/horns-kit": "0.1.0",
58
- "@manta-eu/horns-server": "0.1.0",
59
- "@manta-eu/horns": "0.1.0"
81
+ "@manta-eu/horns": "0.1.1",
82
+ "@manta-eu/horns-kit": "0.1.1",
83
+ "@manta-eu/horns-server": "0.1.1"
60
84
  },
61
85
  "devDependencies": {
62
86
  "@tailwindcss/cli": "^4",
@@ -0,0 +1,9 @@
1
+ // The module `hornsShapeCatalog` serves. A host picks up these types with
2
+ // `/// <reference types="@manta-eu/horns-client/shape-catalog" />`, because a virtual module has no file
3
+ // for TypeScript to find on its own.
4
+ declare module "virtual:horns/shape-catalog" {
5
+ import type { ShapeCatalog } from "@manta-eu/horns/shapes";
6
+
7
+ /** The realm's shapes, as the build resolved them. */
8
+ export const catalog: ShapeCatalog;
9
+ }
@@ -0,0 +1,6 @@
1
+ // The module `hornsTypecheckFs` serves. A host picks up these types with
2
+ // `/// <reference types="@manta-eu/horns-client/typecheck-fs" />`.
3
+ declare module "virtual:horns/typecheck-fs" {
4
+ /** Every `.d.ts` a written view is checked against, keyed by the path the compiler looks for. */
5
+ export const fs: Readonly<Record<string, string>>;
6
+ }
@@ -1,6 +0,0 @@
1
- import type { ReactNode } from "react";
2
- type SessionGateProps = {
3
- children: ReactNode;
4
- };
5
- export declare function SessionGate({ children }: SessionGateProps): ReactNode;
6
- export {};
@@ -1,19 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useLocation } from "react-router";
3
- import { match } from "ts-pattern";
4
- import { sessionGate, signInHref, useMantaSession } from "../lib/manta/session";
5
- function Centered({ children }) {
6
- return _jsx("main", { className: "grid min-h-screen place-items-center p-8 text-center", children: children });
7
- }
8
- export function SessionGate({ children }) {
9
- const session = useMantaSession();
10
- const location = useLocation();
11
- const signIn = signInHref(`${location.pathname}${location.search}`);
12
- return match(sessionGate(session))
13
- .with("loading", () => (_jsx(Centered, { children: _jsx("p", { className: "text-sm opacity-70", children: "Loading\u2026" }) })))
14
- .with("sign-in", () => (_jsx(Centered, { children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("h1", { className: "text-xl font-semibold", children: "Sign in to Manta Horns" }), _jsx("a", { className: "rounded-md border px-4 py-2", href: signIn, children: "Sign in" })] }) })))
15
- .with("looped", () => (_jsx(Centered, { children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("p", { role: "alert", children: "Signing in did not complete. Try again, or ask an admin to check your access." }), _jsx("a", { className: "rounded-md border px-4 py-2", href: signIn, children: "Sign in" })] }) })))
16
- .with("blocked", () => (_jsx(Centered, { children: _jsxs("div", { className: "flex flex-col items-center gap-4", children: [_jsx("p", { role: "alert", children: session.authError ?? session.error ?? "Manta Horns is unavailable." }), _jsx("a", { className: "rounded-md border px-4 py-2", href: signIn, children: "Try again" })] }) })))
17
- .with("ready", () => children)
18
- .exhaustive();
19
- }
package/dist/client.d.ts DELETED
@@ -1,10 +0,0 @@
1
- import { type ShapeCatalogSource } from "./lib/shapes/catalog";
2
- import "./app/globals.css";
3
- export type MountHornsOptions = {
4
- /** The realm's shapes. Horns ships none: they describe a deployment's data model, not the library's. */
5
- shapes: ShapeCatalogSource;
6
- /** Defaults to `#app`, which is what this package's own document declares. */
7
- container?: HTMLElement;
8
- };
9
- /** Mount Horns as a whole page. A host with a router of its own splices `hornsRoutes()` in instead. */
10
- export declare function mountHorns({ shapes, container }: MountHornsOptions): void;
@@ -1,10 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import { QueryClient } from "@tanstack/react-query";
3
- export declare function datastoreSparqlPath(datastoreId: string): string;
4
- export declare function queryClientFor(datastoreId: string): QueryClient;
5
- type MantaSparqlProviderProps = {
6
- datastoreId: string;
7
- children: ReactNode;
8
- };
9
- export declare function MantaSparqlProvider({ datastoreId, children }: MantaSparqlProviderProps): import("react").JSX.Element;
10
- export {};
@@ -1,48 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useMemo } from "react";
3
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
- import { isRecord } from "@manta-eu/horns";
5
- import { SparqlContext, createSparqlEngine, createSparqlOptions } from "./ldo";
6
- export function datastoreSparqlPath(datastoreId) {
7
- return `/d/${encodeURIComponent(datastoreId)}/sparql`;
8
- }
9
- function fetchWithSparqlErrorBody(baseFetch) {
10
- return async (input, init) => {
11
- const response = await baseFetch(input, init);
12
- if (response.ok)
13
- return response;
14
- const text = await response.text();
15
- let detail = text;
16
- try {
17
- const parsed = JSON.parse(text);
18
- if (isRecord(parsed) && typeof parsed.error === "string" && parsed.error.trim())
19
- detail = parsed.error;
20
- }
21
- catch {
22
- // Non-JSON upstream body — keep the raw text.
23
- }
24
- throw new Error(`${response.status} ${response.statusText} — ${detail}`);
25
- };
26
- }
27
- // One cache per datastore: a cache shared across datastores would answer one tenant's rows under
28
- // another tenant's name the moment someone opens a different datastore.
29
- const clientsByDatastore = new Map();
30
- export function queryClientFor(datastoreId) {
31
- const existing = clientsByDatastore.get(datastoreId);
32
- if (existing)
33
- return existing;
34
- const created = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 1 } } });
35
- clientsByDatastore.set(datastoreId, created);
36
- return created;
37
- }
38
- export function MantaSparqlProvider({ datastoreId, children }) {
39
- const client = queryClientFor(datastoreId);
40
- const value = useMemo(() => {
41
- const options = createSparqlOptions({ sparqlEndpoint: datastoreSparqlPath(datastoreId) });
42
- return {
43
- options: { ...options, fetch: fetchWithSparqlErrorBody(options.fetch ?? fetch) },
44
- engine: createSparqlEngine(),
45
- };
46
- }, [datastoreId]);
47
- return (_jsx(QueryClientProvider, { client: client, children: _jsx(SparqlContext.Provider, { value: value, children: children }) }));
48
- }
@@ -1,47 +0,0 @@
1
- import { type ReactNode } from "react";
2
- export type MantaDatastore = {
3
- id: string;
4
- name: string;
5
- depth?: number;
6
- selectable?: boolean;
7
- };
8
- export type MantaSessionUser = {
9
- id: string;
10
- email: string;
11
- name?: string;
12
- isOperator: boolean;
13
- datastore?: MantaDatastore;
14
- };
15
- export type MantaSessionStatus = "loading" | "signed-out" | "ready" | "error";
16
- export type MantaSessionValue = {
17
- status: MantaSessionStatus;
18
- isLive: boolean;
19
- user?: MantaSessionUser;
20
- datastore?: MantaDatastore;
21
- datastores: MantaDatastore[];
22
- isOperator: boolean;
23
- canSwitchDatastore: boolean;
24
- error?: string;
25
- authError?: string;
26
- justSignedIn: boolean;
27
- revision: number;
28
- reload: () => void;
29
- };
30
- export declare function signInHref(redirect?: string): string;
31
- export declare function readJsonError(response: Response, fallback: string): Promise<string>;
32
- export declare class RequestFailed extends Error {
33
- readonly status: number;
34
- constructor(status: number, message: string);
35
- }
36
- export declare function statusOf(caught: unknown): number | undefined;
37
- export declare function requestJson<T>(path: string, init: RequestInit, fallback: string, parse: (body: unknown) => T): Promise<T | undefined>;
38
- export type SessionGate = "loading" | "sign-in" | "looped" | "blocked" | "ready";
39
- export declare function sessionGate(value: Pick<MantaSessionValue, "status" | "authError" | "justSignedIn">): SessionGate;
40
- export declare function realmDatastore(value: Pick<MantaSessionValue, "datastores">): MantaDatastore | undefined;
41
- export declare function activeDatastore(value: Pick<MantaSessionValue, "isOperator" | "user" | "datastores">): MantaDatastore | undefined;
42
- type MantaSessionProviderProps = {
43
- children: ReactNode;
44
- };
45
- export declare function MantaSessionProvider({ children }: MantaSessionProviderProps): import("react").JSX.Element;
46
- export declare function useMantaSession(): MantaSessionValue;
47
- export {};
@@ -1,204 +0,0 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, use, useCallback, useEffect, useMemo, useState } from "react";
3
- import { isPresent } from "@manta-eu/lib/present";
4
- import { isRecord, isString } from "@manta-eu/horns";
5
- import { signedInParam } from "@manta-eu/horns-server/shared";
6
- export function signInHref(redirect) {
7
- const params = new URLSearchParams();
8
- if (isPresent(redirect) && redirect !== "/")
9
- params.set("redirect", redirect);
10
- const query = params.toString();
11
- return `/login${query ? `?${query}` : ""}`;
12
- }
13
- export async function readJsonError(response, fallback) {
14
- const body = await response.json().catch(() => undefined);
15
- if (typeof body === "object" && isPresent(body) && "error" in body && typeof body.error === "string") {
16
- return body.error;
17
- }
18
- return fallback;
19
- }
20
- export class RequestFailed extends Error {
21
- status;
22
- constructor(status, message) {
23
- super(message);
24
- this.name = "RequestFailed";
25
- this.status = status;
26
- }
27
- }
28
- export function statusOf(caught) {
29
- return caught instanceof RequestFailed ? caught.status : undefined;
30
- }
31
- // 204 responses are success, not a parse failure. The body is untrusted until `parse` has checked it.
32
- export async function requestJson(path, init, fallback, parse) {
33
- const response = await fetch(path, { credentials: "same-origin", ...init });
34
- if (!response.ok)
35
- throw new RequestFailed(response.status, await readJsonError(response, fallback));
36
- const text = await response.text();
37
- if (!isPresent(text.trim()))
38
- return undefined;
39
- const body = JSON.parse(text);
40
- return parse(body);
41
- }
42
- const readJson = (path, fallback, parse) => requestJson(path, { headers: { accept: "application/json" } }, fallback, parse);
43
- function parseDatastore(value) {
44
- if (!isRecord(value) || typeof value.id !== "string" || typeof value.name !== "string")
45
- return undefined;
46
- return {
47
- id: value.id,
48
- name: value.name,
49
- ...(typeof value.depth === "number" ? { depth: value.depth } : {}),
50
- ...(typeof value.selectable === "boolean" ? { selectable: value.selectable } : {}),
51
- };
52
- }
53
- function parseLoginStatus(body) {
54
- if (!isRecord(body) || typeof body.valid !== "boolean")
55
- throw new Error("Sign-in status is malformed.");
56
- return {
57
- valid: body.valid,
58
- canSwitchDatastore: body.canSwitchDatastore === true,
59
- allowedModels: Array.isArray(body.allowedModels) ? body.allowedModels.filter(isString) : [],
60
- };
61
- }
62
- function parseUserContext(body) {
63
- if (!isRecord(body) || !isRecord(body.user))
64
- throw new Error("User context is malformed.");
65
- const { user } = body;
66
- if (typeof user.id !== "string" || typeof user.email !== "string")
67
- throw new Error("User context is malformed.");
68
- const datastore = parseDatastore(user.datastore);
69
- return {
70
- user: {
71
- id: user.id,
72
- email: user.email,
73
- isOperator: user.isOperator === true,
74
- ...(typeof user.name === "string" ? { name: user.name } : {}),
75
- ...(datastore ? { datastore } : {}),
76
- },
77
- datastores: Array.isArray(body.datastores) ? body.datastores.flatMap((entry) => parseDatastore(entry) ?? []) : [],
78
- };
79
- }
80
- export function sessionGate(value) {
81
- if (isPresent(value.authError))
82
- return "blocked";
83
- if (value.status === "loading")
84
- return "loading";
85
- if (value.status === "error")
86
- return "blocked";
87
- if (value.status === "signed-out")
88
- return value.justSignedIn ? "looped" : "sign-in";
89
- return "ready";
90
- }
91
- // The realm's own top-level datastore is the one with no depth that cannot be picked.
92
- export function realmDatastore(value) {
93
- return value.datastores.find((datastore) => !isPresent(datastore.depth) && datastore.selectable !== true);
94
- }
95
- export function activeDatastore(value) {
96
- if (isPresent(value.user?.datastore))
97
- return value.user.datastore;
98
- if (!value.isOperator)
99
- return undefined;
100
- return realmDatastore(value);
101
- }
102
- const MantaSessionContext = createContext(undefined);
103
- function readAuthError() {
104
- const raw = new URLSearchParams(window.location.search).get("auth_error");
105
- return isPresent(raw) ? raw : undefined;
106
- }
107
- function readJustSignedIn() {
108
- return new URLSearchParams(window.location.search).get(signedInParam) === "1";
109
- }
110
- export function MantaSessionProvider({ children }) {
111
- const [status, setStatus] = useState("loading");
112
- const [context, setContext] = useState();
113
- const [canSwitchDatastore, setCanSwitchDatastore] = useState(false);
114
- const [error, setError] = useState();
115
- const [reloads, setReloads] = useState(0);
116
- const [revision, setRevision] = useState(0);
117
- const [authError, setAuthError] = useState(readAuthError);
118
- const [justSignedIn] = useState(readJustSignedIn);
119
- useEffect(() => {
120
- if (!isPresent(authError) && !justSignedIn)
121
- return;
122
- const url = new URL(window.location.href);
123
- url.searchParams.delete("auth_error");
124
- url.searchParams.delete(signedInParam);
125
- window.history.replaceState(window.history.state, "", url.pathname + url.search);
126
- }, [authError, justSignedIn]);
127
- useEffect(() => {
128
- let live = true;
129
- void (async () => {
130
- if (reloads === 0)
131
- setStatus("loading");
132
- setError(undefined);
133
- try {
134
- const login = await readJson("/api/login", "Sign-in status is unavailable.", parseLoginStatus);
135
- if (!live)
136
- return;
137
- if (!isPresent(login))
138
- throw new Error("Sign-in status is unavailable.");
139
- setCanSwitchDatastore(login.canSwitchDatastore);
140
- if (!login.valid) {
141
- setContext(undefined);
142
- setStatus("signed-out");
143
- return;
144
- }
145
- const loaded = await readJson("/api/data/context", "User context is unavailable.", parseUserContext);
146
- if (!live)
147
- return;
148
- if (!isPresent(loaded))
149
- throw new Error("User context is unavailable.");
150
- setContext(loaded);
151
- setStatus("ready");
152
- setRevision((count) => count + 1);
153
- }
154
- catch (caught) {
155
- if (!live)
156
- return;
157
- // The upstream session can expire between the /api/login check and this fetch.
158
- if (statusOf(caught) === 401) {
159
- setContext(undefined);
160
- setStatus("signed-out");
161
- return;
162
- }
163
- setError(caught instanceof Error ? caught.message : "Manta Horns is unavailable.");
164
- setStatus("error");
165
- }
166
- })();
167
- return () => {
168
- live = false;
169
- };
170
- }, [reloads]);
171
- const reload = useCallback(() => {
172
- setAuthError(undefined);
173
- setReloads((count) => count + 1);
174
- }, []);
175
- const value = useMemo(() => {
176
- const isOperator = context?.user.isOperator ?? false;
177
- const datastore = activeDatastore({
178
- isOperator,
179
- ...(isPresent(context) ? { user: context.user } : {}),
180
- datastores: context?.datastores ?? [],
181
- });
182
- return {
183
- status,
184
- isLive: status === "ready",
185
- ...(isPresent(context) ? { user: context.user } : {}),
186
- ...(isPresent(datastore) ? { datastore } : {}),
187
- datastores: context?.datastores ?? [],
188
- isOperator,
189
- canSwitchDatastore,
190
- ...(isPresent(error) ? { error } : {}),
191
- ...(isPresent(authError) ? { authError } : {}),
192
- justSignedIn,
193
- revision,
194
- reload,
195
- };
196
- }, [status, context, canSwitchDatastore, error, authError, justSignedIn, revision, reload]);
197
- return _jsx(MantaSessionContext.Provider, { value: value, children: children });
198
- }
199
- export function useMantaSession() {
200
- const value = use(MantaSessionContext);
201
- if (!isPresent(value))
202
- throw new Error("useMantaSession must be used inside MantaSessionProvider");
203
- return value;
204
- }