@fungi.computer/tourtois 0.2.1 → 0.3.0

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.
@@ -2,19 +2,24 @@ import { teamComputerDirectorySchema, type TeamComputer } from "@fungi.computer/
2
2
  import * as z from "zod";
3
3
  /** One owner-fleet page returned by the public Computer controls port. */
4
4
  export type ComputerPage = z.output<typeof teamComputerDirectorySchema>;
5
- /** Owner controls over the existing standalone fleet, not a second inventory. */
5
+ /** Host-supplied owner controls over the existing fleet, not a second inventory. */
6
6
  export type OysterComputersPort = Readonly<{
7
7
  list(offset: number): Promise<ComputerPage>;
8
- selected(): Promise<string | null>;
9
8
  create(name: string | undefined, requestId: string): Promise<TeamComputer>;
10
9
  start(computerId: string): Promise<void>;
11
- select(computerId: string): Promise<TeamComputer>;
12
10
  internetAccess(computerId: string): Promise<boolean>;
13
11
  setInternetAccess(computerId: string, enabled: boolean): Promise<boolean>;
12
+ /** Only hosts with a persisted default for new Apps supply selection. */
13
+ selection?: Readonly<{
14
+ selected(): Promise<string | null>;
15
+ select(computerId: string): Promise<TeamComputer>;
16
+ }>;
17
+ /** Operator policy readback, when the host exposes it. Unknown locks writes. */
18
+ egressPolicy?(): Promise<"enabled" | "globally-disabled" | "team-disabled">;
14
19
  }>;
15
20
  /** Bind owner-admitted fleet controls to this trusted shell's host origin. */
16
21
  export declare function createOysterComputersPort(origin: URL): OysterComputersPort;
17
- /** Owner Settings over the host's existing inventory and default selection. */
22
+ /** Owner Settings over the host's existing inventory and optional default. */
18
23
  export declare function OysterComputers({ teamId, port, onChanged, }: {
19
24
  teamId: string;
20
25
  port: OysterComputersPort;
@@ -40,11 +40,6 @@ export function createOysterComputersPort(origin) {
40
40
  const response = await read(`/api/computers?${new URLSearchParams({ limit: "100", offset: String(offset) })}`);
41
41
  return teamComputerDirectorySchema.parse(await response.json());
42
42
  },
43
- selected: async () => {
44
- const response = await read("/api/workbench/directory");
45
- return (workbenchDirectorySchema.parse(await response.json()).computer
46
- ?.computerId ?? null);
47
- },
48
43
  create: async (name, requestId) => {
49
44
  const response = await read("/api/computers", {
50
45
  method: "POST",
@@ -59,12 +54,19 @@ export function createOysterComputersPort(origin) {
59
54
  });
60
55
  await response.json();
61
56
  },
62
- select: async (computerId) => {
63
- const response = await read("/api/computers/selected", {
64
- method: "PUT",
65
- body: JSON.stringify({ computerId }),
66
- });
67
- return teamComputerSchema.parse(await response.json());
57
+ selection: {
58
+ selected: async () => {
59
+ const response = await read("/api/workbench/directory");
60
+ return (workbenchDirectorySchema.parse(await response.json()).computer
61
+ ?.computerId ?? null);
62
+ },
63
+ select: async (computerId) => {
64
+ const response = await read("/api/computers/selected", {
65
+ method: "PUT",
66
+ body: JSON.stringify({ computerId }),
67
+ });
68
+ return teamComputerSchema.parse(await response.json());
69
+ },
68
70
  },
69
71
  internetAccess: async (computerId) => {
70
72
  const response = await read(`/api/computers/${encodeURIComponent(computerId)}/internet-access`);
@@ -76,7 +78,7 @@ export function createOysterComputersPort(origin) {
76
78
  },
77
79
  };
78
80
  }
79
- function InternetAccess({ teamId, computer, port, }) {
81
+ function InternetAccess({ teamId, computer, port, policy, }) {
80
82
  const queryKey = tourtoisQueryKeys.oysterInternetAccess(teamId, computer.computerId);
81
83
  const current = useQuery({
82
84
  queryKey,
@@ -87,13 +89,15 @@ function InternetAccess({ teamId, computer, port, }) {
87
89
  apply: (_previous, enabled) => enabled,
88
90
  mutationFn: (enabled) => port.setInternetAccess(computer.computerId, enabled),
89
91
  }));
90
- return (_jsxs("div", { className: "grid gap-sm", children: [_jsxs("label", { className: "flex gap-sm", children: ["Internet access", _jsx(Switch, { "aria-label": `Internet access for ${computer.name}`, checked: current.data ?? false, disabled: current.data === undefined || update.isPending, onCheckedChange: (enabled) => {
92
+ return (_jsxs("div", { className: "grid gap-sm", children: [_jsxs("label", { className: "flex gap-sm", children: ["Internet access", _jsx(Switch, { "aria-label": `Internet access for ${computer.name}`, checked: current.data ?? false, disabled: policy !== "enabled" ||
93
+ current.data === undefined ||
94
+ update.isPending, onCheckedChange: (enabled) => {
91
95
  update.mutate(enabled);
92
96
  } })] }), current.isPending ? (_jsx("p", { role: "status", children: "Checking Internet access\u2026" })) : null, current.isError ? (_jsxs("div", { role: "alert", children: [_jsxs("p", { children: ["Could not check Internet access for ", computer.name, "."] }), _jsx(Button, { onClick: () => {
93
97
  void current.refetch();
94
- }, children: "Retry Internet access" })] })) : null, update.isError ? (_jsxs("p", { role: "alert", children: ["Could not save Internet access for ", computer.name, "."] })) : null] }));
98
+ }, children: "Retry Internet access" })] })) : null, update.isError ? (_jsxs("p", { role: "alert", children: ["Could not save Internet access for ", computer.name, "."] })) : null, policy === "globally-disabled" ? (_jsx("p", { children: "Internet access is locked by the global off switch." })) : null, policy === "team-disabled" ? (_jsx("p", { children: "Internet access is locked for this Team." })) : null] }));
95
99
  }
96
- /** Owner Settings over the host's existing inventory and default selection. */
100
+ /** Owner Settings over the host's existing inventory and optional default. */
97
101
  export function OysterComputers({ teamId, port, onChanged, }) {
98
102
  const client = useQueryClient();
99
103
  const [offset, setOffset] = useState(0);
@@ -101,13 +105,24 @@ export function OysterComputers({ teamId, port, onChanged, }) {
101
105
  const intent = useRef(null);
102
106
  const inventoryKey = tourtoisQueryKeys.oysterComputers(teamId);
103
107
  const selectionKey = tourtoisQueryKeys.oysterSelectedComputer(teamId);
108
+ const selection = port.selection;
104
109
  const inventory = useQuery({
105
110
  queryKey: tourtoisQueryKeys.oysterComputerPage(teamId, offset),
106
111
  queryFn: () => port.list(offset),
107
112
  });
108
113
  const selected = useQuery({
109
114
  queryKey: selectionKey,
110
- queryFn: () => port.selected(),
115
+ enabled: selection !== undefined,
116
+ queryFn: () => selection === undefined
117
+ ? Promise.reject(new Error("This host has no default Computer"))
118
+ : selection.selected(),
119
+ });
120
+ const policy = useQuery({
121
+ queryKey: tourtoisQueryKeys.oysterEgressPolicy(teamId),
122
+ enabled: port.egressPolicy !== undefined,
123
+ queryFn: () => port.egressPolicy === undefined
124
+ ? Promise.reject(new Error("This host has no egress policy readback"))
125
+ : port.egressPolicy(),
111
126
  });
112
127
  const action = useMutation({
113
128
  mutationFn: async (input) => {
@@ -119,7 +134,9 @@ export function OysterComputers({ teamId, port, onChanged, }) {
119
134
  await port.start(input.computerId);
120
135
  return;
121
136
  case "select":
122
- await port.select(input.computerId);
137
+ if (selection === undefined)
138
+ throw new Error("This host has no default Computer");
139
+ await selection.select(input.computerId);
123
140
  }
124
141
  },
125
142
  onSuccess: async (_result, input) => {
@@ -130,32 +147,42 @@ export function OysterComputers({ teamId, port, onChanged, }) {
130
147
  }
131
148
  await Promise.all([
132
149
  client.invalidateQueries({ queryKey: inventoryKey }),
133
- client.invalidateQueries({ queryKey: selectionKey }),
150
+ ...(selection === undefined
151
+ ? []
152
+ : [client.invalidateQueries({ queryKey: selectionKey })]),
134
153
  ]);
135
154
  onChanged();
136
155
  },
137
156
  });
138
157
  const unavailable = inventory.isError ||
139
- selected.isError ||
158
+ (selection !== undefined && selected.isError) ||
140
159
  inventory.isFetching ||
141
- selected.isFetching ||
160
+ (selection !== undefined && selected.isFetching) ||
142
161
  action.isPending;
143
- return (_jsxs("section", { className: "grid gap-sm", "aria-label": "Computers", children: [_jsx(SectionHeading, { children: "Computers" }), _jsx("p", { children: "Choose the default Computer used for new App configurations." }), inventory.isPending ? _jsx("p", { role: "status", children: "Loading Computers\u2026" }) : null, inventory.isError || selected.isError ? (_jsxs("div", { role: "alert", children: [_jsx("p", { children: "Computer inventory or selection could not be loaded." }), _jsx(Button, { onClick: () => {
162
+ const policyStatus = port.egressPolicy === undefined
163
+ ? "enabled"
164
+ : (policy.data ?? "unavailable");
165
+ return (_jsxs("section", { className: "grid gap-sm", "aria-label": "Computers", children: [_jsx(SectionHeading, { children: "Computers" }), _jsx("p", { children: selection === undefined
166
+ ? "Manage your Team's Computers."
167
+ : "Choose the default Computer used for new App configurations." }), port.egressPolicy !== undefined && policyStatus === "unavailable" ? (_jsxs("div", { role: "alert", children: [_jsx("p", { children: "Internet access policy is unavailable; switches are locked." }), _jsx(Button, { onClick: () => void policy.refetch(), children: "Retry policy" })] })) : null, inventory.isPending ? _jsx("p", { role: "status", children: "Loading Computers\u2026" }) : null, inventory.isError || (selection !== undefined && selected.isError) ? (_jsxs("div", { role: "alert", children: [_jsx("p", { children: "Computer inventory or selection could not be loaded." }), _jsx(Button, { onClick: () => {
144
168
  void inventory.refetch();
145
- void selected.refetch();
146
- }, children: "Retry" })] })) : null, inventory.isSuccess && inventory.data.computers.length === 0 ? (_jsx("p", { children: "No Computers on this page." })) : null, inventory.data !== undefined ? (_jsx(DataList, { "aria-label": "Computer inventory", children: inventory.data.computers.map((computer) => (_jsxs(DataRow, { className: "p-0", children: [_jsx(ActionRow, { label: computer.name, description: selected.isSuccess && selected.data === computer.computerId
147
- ? "Default for new Apps"
148
- : computer.computerId, trailing: _jsxs("div", { className: "flex gap-sm", children: [_jsx(Button, { "aria-label": `Start ${computer.name}`, disabled: unavailable, onClick: () => {
149
- action.mutate({
150
- kind: "start",
151
- computerId: computer.computerId,
152
- });
153
- }, children: "Start" }), _jsx(Button, { "aria-label": `Use ${computer.name} for new Apps`, disabled: unavailable || selected.data === computer.computerId, onClick: () => {
154
- action.mutate({
155
- kind: "select",
156
- computerId: computer.computerId,
157
- });
158
- }, children: "Use for new Apps" })] }) }), _jsx(InternetAccess, { teamId: teamId, computer: computer, port: port })] }, computer.computerId))) })) : null, _jsxs("div", { className: "flex gap-sm", "aria-label": "Computer pages", children: [_jsx(Button, { disabled: unavailable || offset === 0, onClick: () => {
169
+ if (selection !== undefined)
170
+ void selected.refetch();
171
+ }, children: "Retry" })] })) : null, inventory.isSuccess && inventory.data.computers.length === 0 ? (_jsx("p", { children: "No Computers on this page." })) : null, inventory.data !== undefined ? (_jsx(DataList, { "aria-label": "Computer inventory", children: inventory.data.computers.map((computer) => (_jsx(DataRow, { className: "p-0", children: _jsxs("div", { className: "grid min-w-0 gap-sm", children: [_jsx(ActionRow, { label: computer.name, description: selection !== undefined &&
172
+ selected.isSuccess &&
173
+ selected.data === computer.computerId
174
+ ? "Default for new Apps"
175
+ : computer.computerId, trailing: _jsxs("div", { className: "flex flex-wrap gap-sm", children: [_jsx(Button, { "aria-label": `Start ${computer.name}`, disabled: unavailable, onClick: () => {
176
+ action.mutate({
177
+ kind: "start",
178
+ computerId: computer.computerId,
179
+ });
180
+ }, children: "Start" }), selection !== undefined ? (_jsx(Button, { "aria-label": `Use ${computer.name} for new Apps`, disabled: unavailable || selected.data === computer.computerId, onClick: () => {
181
+ action.mutate({
182
+ kind: "select",
183
+ computerId: computer.computerId,
184
+ });
185
+ }, children: "Use for new Apps" })) : null] }) }), _jsx(InternetAccess, { teamId: teamId, computer: computer, port: port, policy: policyStatus })] }) }, computer.computerId))) })) : null, _jsxs("div", { className: "flex gap-sm", "aria-label": "Computer pages", children: [_jsx(Button, { disabled: unavailable || offset === 0, onClick: () => {
159
186
  setOffset(Math.max(0, offset - 100));
160
187
  }, children: "Previous" }), _jsx(Button, { disabled: unavailable || inventory.data?.nextOffset == null, onClick: () => {
161
188
  const next = inventory.data?.nextOffset;
package/dist/queries.d.ts CHANGED
@@ -4,6 +4,7 @@ export declare const tourtoisQueryKeys: {
4
4
  oysterComputerPage: (teamId: string, offset: number) => readonly ["tourtois", "oyster-computers", string, number];
5
5
  oysterSelectedComputer: (teamId: string) => readonly ["tourtois", "oyster-selected-computer", string];
6
6
  oysterInternetAccess: (teamId: string, computerId: string) => readonly ["tourtois", "oyster-internet-access", string, string];
7
+ oysterEgressPolicy: (teamId: string) => readonly ["tourtois", "oyster-egress-policy", string];
7
8
  hostSettings: (workspaceId: string) => readonly ["tourtois", "host-settings", string];
8
9
  workbench: () => readonly ["tourtois", "workbench"];
9
10
  workbenchComputers: (teamId: string) => readonly ["tourtois", "workbench-computers", string];
package/dist/queries.js CHANGED
@@ -4,6 +4,7 @@ export const tourtoisQueryKeys = {
4
4
  oysterComputerPage: (teamId, offset) => [...tourtoisQueryKeys.oysterComputers(teamId), offset],
5
5
  oysterSelectedComputer: (teamId) => ["tourtois", "oyster-selected-computer", teamId],
6
6
  oysterInternetAccess: (teamId, computerId) => ["tourtois", "oyster-internet-access", teamId, computerId],
7
+ oysterEgressPolicy: (teamId) => ["tourtois", "oyster-egress-policy", teamId],
7
8
  hostSettings: (workspaceId) => ["tourtois", "host-settings", workspaceId],
8
9
  workbench: () => ["tourtois", "workbench"],
9
10
  workbenchComputers: (teamId) => ["tourtois", "workbench-computers", teamId],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/tourtois",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "Tourtois: the trusted desktop shell for Oyster and Fungi, with host-supplied Settings sections",
6
6
  "type": "module",
@@ -34,6 +34,10 @@
34
34
  "types": "./dist/platform-controls.d.ts",
35
35
  "import": "./dist/platform-controls.js"
36
36
  },
37
+ "./oyster-computers": {
38
+ "types": "./dist/OysterComputers.d.ts",
39
+ "import": "./dist/OysterComputers.js"
40
+ },
37
41
  "./settings-routes": {
38
42
  "types": "./dist/settings-routes.d.ts",
39
43
  "import": "./dist/settings-routes.js"
@@ -69,15 +73,15 @@
69
73
  "LICENSE"
70
74
  ],
71
75
  "dependencies": {
72
- "@fungi.computer/agar": "0.3.0",
76
+ "@fungi.computer/agar": "0.3.1",
73
77
  "@fungi.computer/cairn": "0.1.0",
74
- "@fungi.computer/caps": "0.2.0",
78
+ "@fungi.computer/caps": "0.3.0",
75
79
  "@fungi.computer/ear-tag": "0.1.0",
76
80
  "@fungi.computer/gills": "0.2.0",
77
81
  "@fungi.computer/maosaico": "0.1.0",
78
- "@fungi.computer/parakeet": "0.2.1",
82
+ "@fungi.computer/parakeet": "0.3.0",
79
83
  "@fungi.computer/puffball": "0.2.0",
80
- "@fungi.computer/shiitake": "0.3.1",
84
+ "@fungi.computer/shiitake": "0.4.0",
81
85
  "@fungi.computer/sigils": "0.2.0",
82
86
  "@fungi.computer/sprite": "0.2.0",
83
87
  "@fungi.computer/sprite-zdog": "0.2.0",
@@ -92,16 +96,16 @@
92
96
  "lucide-react": "0.460.0",
93
97
  "react-dom": "18.3.1",
94
98
  "zod": "4.4.3",
95
- "@fungi.computer/nacre": "0.0.1"
99
+ "@fungi.computer/nacre": "0.0.2"
96
100
  },
97
101
  "peerDependencies": {
98
102
  "react": "^18.3.1 || ^19.2.0"
99
103
  },
100
104
  "devDependencies": {
101
- "@fungi.computer/oyster": "0.3.2",
105
+ "@fungi.computer/oyster": "0.4.0",
102
106
  "@cloudflare/workers-types": "5.20260828.1",
103
107
  "@fungi.computer/forage": "0.2.0",
104
- "@fungi.computer/mycelium": "0.2.1",
108
+ "@fungi.computer/mycelium": "0.2.2",
105
109
  "@ladle/react": "5.1.1",
106
110
  "@types/react": "18.3.3",
107
111
  "@vitejs/plugin-react": "6.1.1",