cortena-ui 1.14.0 → 1.15.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.
- package/CHANGELOG.md +103 -0
- package/README.md +205 -0
- package/dist/app.d.ts +6 -0
- package/dist/app.js +5 -0
- package/dist/components/admin-permissions/index.js +11 -0
- package/dist/components/app/agent-mount.d.ts +53 -0
- package/dist/components/app/agent-mount.js +40 -0
- package/dist/components/app/agent-mount.js.map +1 -0
- package/dist/components/app/cortena-app.d.ts +184 -0
- package/dist/components/app/cortena-app.js +422 -0
- package/dist/components/app/cortena-app.js.map +1 -0
- package/dist/components/app/launch.d.ts +128 -0
- package/dist/components/app/launch.js +149 -0
- package/dist/components/app/launch.js.map +1 -0
- package/dist/components/app/settings.d.ts +132 -0
- package/dist/components/app/settings.js +424 -0
- package/dist/components/app/settings.js.map +1 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/chip.d.ts +1 -1
- package/dist/components/data-table/use-data-table.js +1 -19
- package/dist/components/data-table/use-data-table.js.map +1 -1
- package/dist/core.d.ts +3 -3
- package/dist/index.d.ts +5 -5
- package/dist/lib/dev.js +44 -0
- package/dist/lib/dev.js.map +1 -0
- package/package.json +5 -1
- package/src/components/app/agent-mount.tsx +103 -0
- package/src/components/app/cortena-app.tsx +783 -0
- package/src/components/app/launch.ts +227 -0
- package/src/components/app/settings.tsx +669 -0
- package/src/components/data-table/use-data-table.ts +1 -19
- package/src/entries/app.ts +73 -0
- package/src/index.ts +12 -5
- package/src/lib/dev.ts +48 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,109 @@
|
|
|
3
3
|
Notable changes per release. Versions before 1.6.0 are recorded in the git log
|
|
4
4
|
and in `../../CONSUMING.md`; this file starts where the changelog does.
|
|
5
5
|
|
|
6
|
+
## 1.15.0
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`cortena-ui/app` — `CortenaApp`, the whole frame** (DESIGN-119). Everything
|
|
11
|
+
around an extension's content, mounted once instead of assembled eight ways:
|
|
12
|
+
the launch, `LoginScreen` with the methods the org advertises, the signed-out
|
|
13
|
+
state with a `returnTo`, exactly one session guard, `AppShell`,
|
|
14
|
+
`AgentChatPopup` in `agentSlot`, the tab title, and the standard Profile /
|
|
15
|
+
Settings / Permissions pages. The 2026-09-14 re-audit is what it replaces:
|
|
16
|
+
seven local login pages across eight extensions, three of them with dummy
|
|
17
|
+
users; a guard mounted with a different subset of its behaviour in each; a
|
|
18
|
+
pop-up mounted without the user's token or the agent slug; a Settings item
|
|
19
|
+
greyed out in every one of the six shell swaps.
|
|
20
|
+
- **A `host` role.** `role="host"` leaves the agent slot empty, because
|
|
21
|
+
CortenaWeb *is* the chat and its own guard is the one that renews and pushes
|
|
22
|
+
tokens into framed extensions. Host-only navigation is `nav` and `headerSlot`
|
|
23
|
+
configuration, not a special case inside the composition. cortena-auth's
|
|
24
|
+
hosted pages are the third consumer.
|
|
25
|
+
- **`readCortenaLaunch({ hostOrigins })`**, and with it the rule that a framed
|
|
26
|
+
extension trusts the host's guard **only when the host declares it**. The
|
|
27
|
+
declaration — a `cortenaGuard=host` parameter beside the token, or the first
|
|
28
|
+
bridge message — is origin-checked against an allow-list the app supplies
|
|
29
|
+
from its own configuration, never from the request. Declared: no guard here,
|
|
30
|
+
and renewed tokens arrive through `session.onTokenRenewed`. Absent: a
|
|
31
|
+
countdown from the access token's own `exp`, the standard warning with no
|
|
32
|
+
Continue, then the signed-out state. Not framed: the full `SessionGuard`. One
|
|
33
|
+
of the three, ever — never two countdowns for one session, and never the old
|
|
34
|
+
behaviour of inferring "the host is guarding me" from the absence of a
|
|
35
|
+
refresh token.
|
|
36
|
+
- **The standard pages**, at `/me`, `/settings` and `/settings/permissions`:
|
|
37
|
+
Profile, then Permissions from `AdminPermissions` against the §14.3 admin
|
|
38
|
+
routes when the caller may administer, then the extension's own settings from
|
|
39
|
+
a declared zod schema — or one plain line, "Nothing to configure for <name>
|
|
40
|
+
yet." The avatar menu's Settings item is never disabled again. Exported
|
|
41
|
+
individually as `CortenaSettingsPage`, `CortenaProfilePage` and
|
|
42
|
+
`ProfileSection`, with `matchCortenaStandardRoute` for an app that would
|
|
43
|
+
rather place them under its own router.
|
|
44
|
+
- **`session.hostOrigins`**, and with it the bridge path for the declaration:
|
|
45
|
+
the composition listens for the host's first
|
|
46
|
+
`{ type: "cortena-session", guarded, accessToken }` and takes the declaration
|
|
47
|
+
and any renewal from it. Prefer this over the launch parameter —
|
|
48
|
+
`event.origin` is the browser's and cannot be forged from inside the sending
|
|
49
|
+
page, where the parameter's check falls back to `document.referrer` on a
|
|
50
|
+
browser with no `ancestorOrigins`. The gap is downgrade-only (a forged
|
|
51
|
+
declaration mounts *no* guard, never a second one) and is written down in
|
|
52
|
+
`launch.ts`.
|
|
53
|
+
- **`session.expiresAt`**, for a token whose `exp` cannot be read — an opaque
|
|
54
|
+
token, or one the decoder failed on. Exactly `SessionToken`'s own escape
|
|
55
|
+
hatch, and the way to keep such a session guarded rather than landing on the
|
|
56
|
+
signed-out screen.
|
|
57
|
+
- **A development warning** where a non-framed launch omits `session.refresh`.
|
|
58
|
+
Without it the guard reaches the proactive refresh at about 75% of the access
|
|
59
|
+
token's life, is rejected, and signs the user out — §10.4.1's half-dead
|
|
60
|
+
screen arriving from the inside, with nothing on screen to explain it.
|
|
61
|
+
- **`CortenaApp` is router- and environment-agnostic**: `pathname` in,
|
|
62
|
+
`onNavigate` out, real `href`s, configuration and tokens as props, no browser
|
|
63
|
+
API touched during a first render, and `documentTitle={false}` for a surface
|
|
64
|
+
that does not own the tab. That is what lets CortenaWeb adopt it inside a
|
|
65
|
+
Next.js root layout instead of keeping a second shell.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- **A signed-out screen you could not leave.** The state was cleared only where
|
|
70
|
+
a *host* pushed a token, and that path exists only in a host-guarded frame.
|
|
71
|
+
On a standalone launch or an undeclared frame the signed-out screen
|
|
72
|
+
short-circuits the whole render, so a successful sign-in set the token and
|
|
73
|
+
the user stayed on "You were signed out" for ever with nothing on screen to
|
|
74
|
+
explain it. A new token now clears it wherever it comes from — the launch,
|
|
75
|
+
the app's own store, `session.onTokenRenewed`, or the bridge.
|
|
76
|
+
- **A frame with an unreadable token was silently unguarded.** A token with no
|
|
77
|
+
readable `exp` took the countdown branch and then counted nothing: no
|
|
78
|
+
warning, no expiry, no signed-out state — the exact state decision (b)
|
|
79
|
+
removed, arriving through the back door. It is now reported as already over,
|
|
80
|
+
which lands on the signed-out screen with a `returnTo`, and says why in a
|
|
81
|
+
development warning. Warning rather than expiring was considered and
|
|
82
|
+
rejected: a warning's entire content is a number there is none of, and its
|
|
83
|
+
only way out is Log out, which lands on the same screen anyway.
|
|
84
|
+
- **A rejected sign-out** left an unhandled rejection and the user on a shell
|
|
85
|
+
whose session was over. It is caught, and the screen stops pretending either
|
|
86
|
+
way — `SessionGuard` makes the same call.
|
|
87
|
+
- **`settings.load()` is no longer called** for a declaration with no readable
|
|
88
|
+
fields. The card renders one sentence; a GET behind it is a request nothing
|
|
89
|
+
on screen is waiting for.
|
|
90
|
+
|
|
91
|
+
### Notes
|
|
92
|
+
|
|
93
|
+
- **The frame is light and stays light.** `cortena-ui/app` measures **285.6 kB
|
|
94
|
+
eager** against a 340 kB ceiling — the shell probe's 212.8 kB plus the
|
|
95
|
+
standard pages — with **no heavy package eager at all**. `AdminPermissions`
|
|
96
|
+
(TanStack Table, and exceljs behind `DataTable`'s export) and the agent mount
|
|
97
|
+
(`AgentChat`, react-markdown, the A2UI renderer) are both `React.lazy`, and
|
|
98
|
+
`test/bundle-probe.test.mjs` measures that rather than assuming it.
|
|
99
|
+
- **`@ag-ui/client` is still reachable from `cortena-ui/agent-chat` alone.** The
|
|
100
|
+
transport factory is passed in as `agent.createClient` rather than imported,
|
|
101
|
+
so an extension writes one named import and the composition still owns every
|
|
102
|
+
argument the pop-up is built with.
|
|
103
|
+
- **This package imports no zod.** A settings schema is typed structurally and
|
|
104
|
+
only ever called — `safeParse` to validate, `.shape` to derive fields — so it
|
|
105
|
+
works across zod 3 and 4 and costs the frame nothing.
|
|
106
|
+
- The favicon and the `<title>` no longer need hand-copied files: the generator
|
|
107
|
+
ships as a Vite plugin from `cortena-design/vite` (cortena-design 3.6.0).
|
|
108
|
+
|
|
6
109
|
## 1.14.0
|
|
7
110
|
|
|
8
111
|
### Added
|
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ consumer's bundler getting tree-shaking right:
|
|
|
36
36
|
| `cortena-ui/a2ui` | the A2UI catalogue and renderer | most of the package, by design |
|
|
37
37
|
| `cortena-ui/sortable-list` | `SortableList`, `SortableHandle`, `arrayMove` | dnd-kit |
|
|
38
38
|
| `cortena-ui/agent-chat` | `AgentChatPopup`, `AgentChat`, `createAguiAgentChatClient` | the A2UI catalogue, plus `@ag-ui/client` (rxjs, zod 3, uuid, protobuf) |
|
|
39
|
+
| `cortena-ui/app` | `CortenaApp` — the whole frame, and `readCortenaLaunch` | 285.6 kB eagerly; the standard pages that are heavy arrive in chunks of their own |
|
|
39
40
|
| `cortena-ui/rich-text-editor` | `RichTextEditor`, `parseMarkdown`, `serializeMarkdown` | TipTap on ProseMirror, 619 kB — the heaviest entry in the package |
|
|
40
41
|
|
|
41
42
|
Each component has exactly one home, so the barrel re-exports every entry
|
|
@@ -129,6 +130,210 @@ regression; `test/chart-bundle.test.tsx` says which line caused it.
|
|
|
129
130
|
check in light, dark and system-dark; add an entry with every variant and
|
|
130
131
|
size when adding a component.
|
|
131
132
|
|
|
133
|
+
## The frame
|
|
134
|
+
|
|
135
|
+
`AppShell` owns what renders after sign-in. Everything around it — the sign-in
|
|
136
|
+
screen, the signed-out state, the session guard, the agent pop-up, the tab
|
|
137
|
+
title and the Profile / Settings / Permissions pages — used to be each
|
|
138
|
+
extension's to assemble, and a 2026-09-14 re-audit of eight of them found eight
|
|
139
|
+
answers: seven local login pages, three still with dummy users; a guard mounted
|
|
140
|
+
with a different subset of its behaviour in each; a pop-up mounted without the
|
|
141
|
+
user's token or the agent slug; a Settings item greyed out in every shell.
|
|
142
|
+
|
|
143
|
+
`cortena-ui/app` is the assembly (DESIGN-119):
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
// main.tsx — synchronously, before createRoot (§7.3)
|
|
147
|
+
import { readCortenaLaunch } from "cortena-ui/app";
|
|
148
|
+
const launch = readCortenaLaunch({ hostOrigins: [config.cortenaWebOrigin] });
|
|
149
|
+
|
|
150
|
+
// App.tsx
|
|
151
|
+
import { CortenaApp } from "cortena-ui/app";
|
|
152
|
+
import { createAguiAgentChatClient } from "cortena-ui/agent-chat";
|
|
153
|
+
|
|
154
|
+
<CortenaApp
|
|
155
|
+
extension={{ id: "tasks", name: "Tasks" }}
|
|
156
|
+
user={user}
|
|
157
|
+
auth={{ publicAuthUrl, orgId, methods, onPassword, onProvider }}
|
|
158
|
+
session={{ launch, getToken, refresh, signOut, onTokenRenewed }}
|
|
159
|
+
nav={{ groups, pathname, onNavigate: (item, e) => { e.preventDefault(); navigate(item.href); } }}
|
|
160
|
+
agent={{ slug: "tasks", gatewayUrl: "/api/agent", createClient: createAguiAgentChatClient, relay, tokensCss }}
|
|
161
|
+
permissions={{ api: adminApi, canAdminister: isAdmin }}
|
|
162
|
+
settings={{ schema, load, save }}
|
|
163
|
+
help={{ source: HELP_DOCUMENT_URL }}
|
|
164
|
+
pathname={pathname}
|
|
165
|
+
onNavigate={(href) => navigate(href)}
|
|
166
|
+
documentTitle={pageTitle}
|
|
167
|
+
>
|
|
168
|
+
<Outlet />
|
|
169
|
+
</CortenaApp>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
It does things in this order, and the order is the contract:
|
|
173
|
+
|
|
174
|
+
1. the launch, from `session.launch`;
|
|
175
|
+
2. no token → `LoginScreen` with the methods the org advertises;
|
|
176
|
+
3. the session ended → `LoginScreen` with `signedOut` and a `returnTo`;
|
|
177
|
+
4. exactly one session guard, at the top, above the shell and the pop-up alike;
|
|
178
|
+
5. `AppShell`, with Profile / Settings / Log out in the avatar menu;
|
|
179
|
+
6. `AgentChatPopup` in `agentSlot`, in the `extension` role only;
|
|
180
|
+
7. the standard pages when `pathname` names one, your `children` otherwise.
|
|
181
|
+
|
|
182
|
+
### It is a client component, and router-agnostic
|
|
183
|
+
|
|
184
|
+
Navigation is `pathname` in and `onNavigate` out, over real `href`s, exactly as
|
|
185
|
+
`AppShell` already does. Configuration and tokens are props: the composition
|
|
186
|
+
fetches nothing, reads no browser API during a first render, and does not
|
|
187
|
+
assume it owns the document — `documentTitle={false}` turns off even the tab
|
|
188
|
+
title. That is not style. The third consumer is CortenaWeb, in a `host` role,
|
|
189
|
+
inside its own Next.js root layout; without every one of those the fleet keeps
|
|
190
|
+
two shells.
|
|
191
|
+
|
|
192
|
+
`role="host"` leaves the agent slot empty, because the host *is* the chat, and
|
|
193
|
+
its own guard is the one that renews and pushes tokens into framed extensions.
|
|
194
|
+
Host-only navigation — an Apps list, an org switcher, platform admin — is `nav`
|
|
195
|
+
and `headerSlot` configuration, not a special case in here.
|
|
196
|
+
|
|
197
|
+
### Reading the launch, and who guards the session
|
|
198
|
+
|
|
199
|
+
A framed extension cannot refresh: CortenaWeb holds the refresh token and hands
|
|
200
|
+
down access tokens (`refreshToken=none`). Inferring "the host is guarding me"
|
|
201
|
+
from that absence is trust by assumption — a frame opened by anything at all
|
|
202
|
+
got no warning and no signed-out state, and nothing said so. So:
|
|
203
|
+
|
|
204
|
+
| the launch | what mounts |
|
|
205
|
+
| --- | --- |
|
|
206
|
+
| framed, and the host **declared** it guards the session | no guard here; renewed tokens arrive through `session.onTokenRenewed` |
|
|
207
|
+
| framed, and it did not | a countdown from the access token's own `exp`, the standard warning with no Continue, then the signed-out state with a `returnTo` |
|
|
208
|
+
| not framed | the full `SessionGuard`: idle clock, proactive refresh, cross-tab `BroadcastChannel` |
|
|
209
|
+
|
|
210
|
+
One of the three, ever. Never two countdowns for one session.
|
|
211
|
+
|
|
212
|
+
There are two ways the declaration can arrive, and they are not equally strong.
|
|
213
|
+
|
|
214
|
+
```tsx
|
|
215
|
+
// The launch parameter: ?token=…&cortenaGuard=host
|
|
216
|
+
const launch = readCortenaLaunch({ hostOrigins: [config.cortenaWebOrigin] });
|
|
217
|
+
|
|
218
|
+
// The first bridge message. Give the same allow-list to the composition and it
|
|
219
|
+
// listens for `{ type: "cortena-session", guarded: true, accessToken? }` itself.
|
|
220
|
+
<CortenaApp session={{ launch, hostOrigins: [config.cortenaWebOrigin], … }} />
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Both are checked against an allow-list **your app supplies from its own
|
|
224
|
+
configuration**, never against anything in the request. With no `hostOrigins`
|
|
225
|
+
nothing is ever trusted, which is the safe direction: the frame then runs its
|
|
226
|
+
own countdown rather than trusting a parent it cannot name.
|
|
227
|
+
|
|
228
|
+
**Prefer the bridge.** `readCortenaSessionMessage` checks `event.origin`, which
|
|
229
|
+
the browser sets from the sending document and which cannot be forged from
|
|
230
|
+
inside the page that sent it. The launch parameter is checked against
|
|
231
|
+
`ancestorOrigins`, which is WebKit/Blink only; everywhere else the input is
|
|
232
|
+
`document.referrer`, and an open redirect on the CortenaWeb origin can produce
|
|
233
|
+
one that reads as CortenaWeb's. The failure is downgrade-only — a forged
|
|
234
|
+
declaration mounts *no* guard, it cannot mount a second one or obtain a token —
|
|
235
|
+
but "no guard" is the state decision (b) exists to remove, so pass
|
|
236
|
+
`hostOrigins` and let the host say it on the bridge. `launch.ts` carries the
|
|
237
|
+
whole note.
|
|
238
|
+
|
|
239
|
+
Token renewals arrive the same way: over the bridge where `hostOrigins` is set,
|
|
240
|
+
or through `session.onTokenRenewed` for an app that would rather own the
|
|
241
|
+
listener. Either one, and a token the app itself signs in with, clears the
|
|
242
|
+
signed-out state.
|
|
243
|
+
|
|
244
|
+
Call it in your store's initialiser, synchronously, before `createRoot`. §7.3
|
|
245
|
+
is the single most common embedding failure: a token read in an effect is read
|
|
246
|
+
after a `ProtectedRoute` has already redirected and stripped the query string,
|
|
247
|
+
and the user sees a login page inside CortenaWeb with nothing to explain it.
|
|
248
|
+
|
|
249
|
+
`session.signOut` is optional, and omitting it is meaningful: in a framed launch
|
|
250
|
+
with no host sign-out message to send, the avatar menu's Log out is disabled
|
|
251
|
+
rather than silently doing nothing.
|
|
252
|
+
|
|
253
|
+
### The standard pages
|
|
254
|
+
|
|
255
|
+
`/me`, `/settings` and `/settings/permissions` are the composition's, reachable
|
|
256
|
+
from the avatar menu, and the Settings item is **never** disabled:
|
|
257
|
+
|
|
258
|
+
- **Profile** — the person's own name, avatar and account link. cortena-auth
|
|
259
|
+
owns the user record, so the page shows it and links to it rather than
|
|
260
|
+
pretending an extension can edit it.
|
|
261
|
+
- **Permissions** — `AdminPermissions` against the §14.3 admin routes, rendered
|
|
262
|
+
only when `permissions.canAdminister`. Absent otherwise, not disabled: "you
|
|
263
|
+
may not" is not a screen. The admin tabs push
|
|
264
|
+
`/settings/permissions/<screen>`, so they are deep-linkable.
|
|
265
|
+
|
|
266
|
+
**`canAdminister` comes from your own permissions client, against the §14.3
|
|
267
|
+
routes — never from an `orgRole` claim on the JWT.** Whether somebody may
|
|
268
|
+
administer *your extension* is your `may()`'s answer over your own matrix
|
|
269
|
+
(§14.2, §14.4); cortena-auth's org role says who owns the organisation, which
|
|
270
|
+
is a different question and one the bootstrap already answered once. Reading
|
|
271
|
+
the claim looks equivalent, hides the screen from an administrator your
|
|
272
|
+
matrix granted, shows it to an org owner your matrix did not, and neither is
|
|
273
|
+
visible until somebody complains. This flag only decides whether the section
|
|
274
|
+
renders; the routes behind it enforce the real thing either way.
|
|
275
|
+
- **The extension's own settings** — whatever it declares, or one plain line:
|
|
276
|
+
"Nothing to configure for <name> yet." An extension that later gains settings
|
|
277
|
+
declares a schema rather than building a page.
|
|
278
|
+
|
|
279
|
+
The routing is your router's. The composition compares `pathname` to the three
|
|
280
|
+
paths and renders its own page instead of `children` when one matches, so it
|
|
281
|
+
works under any router at all — mount it as a layout and give the router a
|
|
282
|
+
route for those paths that renders nothing of its own:
|
|
283
|
+
|
|
284
|
+
```tsx
|
|
285
|
+
// react-router
|
|
286
|
+
<Route element={<Frame />}>
|
|
287
|
+
<Route path="/me" element={null} />
|
|
288
|
+
<Route path="/settings/*" element={null} />
|
|
289
|
+
…
|
|
290
|
+
</Route>
|
|
291
|
+
|
|
292
|
+
// Next.js: app/me/page.tsx and app/settings/[[...rest]]/page.tsx, each
|
|
293
|
+
// rendering null. The frame is in the layout, and it is "use client".
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
`matchCortenaStandardRoute(pathname)` is exported for an app that would rather
|
|
297
|
+
place `CortenaSettingsPage` and `CortenaProfilePage` itself: it is the same
|
|
298
|
+
rule, not a second copy of it.
|
|
299
|
+
|
|
300
|
+
### The settings declaration
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
const settings = z.object({
|
|
304
|
+
alertThreshold: z.number().describe("Raise an alert when a stream falls this far behind."),
|
|
305
|
+
pauseOnFailure: z.boolean(),
|
|
306
|
+
alertChannel: z.enum(["email", "channels", "none"]),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
<CortenaApp settings={{ schema: settings, load, save }} … />
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The fields are derived from the object's shape, and `safeParse` validates a
|
|
313
|
+
save. This package imports no zod: the schema is typed structurally and only
|
|
314
|
+
ever called, so it works across zod 3 and 4, whose internals differ, and costs
|
|
315
|
+
the frame nothing. Reading the shape is deliberately tolerant — a schema it
|
|
316
|
+
cannot read gives no fields, which draws the "nothing to configure" line rather
|
|
317
|
+
than throwing on a settings page.
|
|
318
|
+
|
|
319
|
+
Pass `fields` where the schema alone does not say enough — a select's options,
|
|
320
|
+
a longer description, a textarea. `load` and `save` are the extension's own
|
|
321
|
+
`/settings` route; the composition never stores anything.
|
|
322
|
+
|
|
323
|
+
### What it does not do
|
|
324
|
+
|
|
325
|
+
There is **no consent route**. cortena-auth is the authorisation server and
|
|
326
|
+
renders consent; the content comes from the `scopes` section of the extension's
|
|
327
|
+
own `x-cortena` identity block, which cortena-auth reads from the registered
|
|
328
|
+
in-cluster address. An extension ships no consent screen.
|
|
329
|
+
|
|
330
|
+
And `@ag-ui/client` is reachable from `cortena-ui/agent-chat` and nowhere else,
|
|
331
|
+
including here. The transport factory is a prop — `agent.createClient` — rather
|
|
332
|
+
than an import, which is one named import for the extension and leaves the
|
|
333
|
+
composition owning every argument the pop-up is built with: the user's token,
|
|
334
|
+
read fresh on every request, and the agent slug. Those are the two an extension
|
|
335
|
+
shipped without.
|
|
336
|
+
|
|
132
337
|
## Shell chrome
|
|
133
338
|
|
|
134
339
|
`AppShell` is the chrome every extension wears (§9 of
|
package/dist/app.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { CORTENA_GUARD_HOST, CORTENA_GUARD_PARAM, CORTENA_SESSION_MESSAGE, CortenaLaunch, CortenaSessionMessage, ReadCortenaLaunchOptions, isHostGuardDeclared, readCortenaLaunch, readCortenaSessionMessage } from "./components/app/launch.js";
|
|
3
|
+
import { CortenaAppAgent } from "./components/app/agent-mount.js";
|
|
4
|
+
import { CortenaAppPermissions, CortenaAppUser, CortenaProfilePage, CortenaSettingsControl, CortenaSettingsDeclaration, CortenaSettingsField, CortenaSettingsPage, CortenaSettingsPageProps, CortenaSettingsSchema, ProfileSection, ProfileSectionProps, settingsFields } from "./components/app/settings.js";
|
|
5
|
+
import { CORTENA_STANDARD_ROUTES, CortenaApp, CortenaAppAuth, CortenaAppExtension, CortenaAppProps, CortenaAppRoutes, CortenaSessionAdapter, CortenaStandardRoute, matchCortenaStandardRoute } from "./components/app/cortena-app.js";
|
|
6
|
+
export { CORTENA_GUARD_HOST, CORTENA_GUARD_PARAM, CORTENA_SESSION_MESSAGE, CORTENA_STANDARD_ROUTES, CortenaApp, type CortenaAppAgent, type CortenaAppAuth, type CortenaAppExtension, type CortenaAppPermissions, type CortenaAppProps, type CortenaAppRoutes, type CortenaAppUser, type CortenaLaunch, CortenaProfilePage, type CortenaSessionAdapter, type CortenaSessionMessage, type CortenaSettingsControl, type CortenaSettingsDeclaration, type CortenaSettingsField, CortenaSettingsPage, type CortenaSettingsPageProps, type CortenaSettingsSchema, type CortenaStandardRoute, ProfileSection, type ProfileSectionProps, type ReadCortenaLaunchOptions, isHostGuardDeclared, matchCortenaStandardRoute, readCortenaLaunch, readCortenaSessionMessage, settingsFields };
|
package/dist/app.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { CORTENA_GUARD_HOST, CORTENA_GUARD_PARAM, CORTENA_SESSION_MESSAGE, isHostGuardDeclared, readCortenaLaunch, readCortenaSessionMessage } from "./components/app/launch.js";
|
|
3
|
+
import { CortenaProfilePage, CortenaSettingsPage, ProfileSection, settingsFields } from "./components/app/settings.js";
|
|
4
|
+
import { CORTENA_STANDARD_ROUTES, CortenaApp, matchCortenaStandardRoute } from "./components/app/cortena-app.js";
|
|
5
|
+
export { CORTENA_GUARD_HOST, CORTENA_GUARD_PARAM, CORTENA_SESSION_MESSAGE, CORTENA_STANDARD_ROUTES, CortenaApp, CortenaProfilePage, CortenaSettingsPage, ProfileSection, isHostGuardDeclared, matchCortenaStandardRoute, readCortenaLaunch, readCortenaSessionMessage, settingsFields };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
import { ADMIN_ROUTES, ADMIN_ROUTE_LABELS, adminErrorMessage, effectiveRolesOf, sourceGroupOf } from "./types.js";
|
|
4
|
+
import { AdminPermissionsProvider, useAdminPermissions } from "./context.js";
|
|
5
|
+
import { AdminLicenceScreen } from "./licence.js";
|
|
6
|
+
import { AdminMatrix } from "./matrix.js";
|
|
7
|
+
import { DirectRoles, EffectiveRoles, RoleAssignmentDialog } from "./role-assignment.js";
|
|
8
|
+
import { AdminMembers } from "./members.js";
|
|
9
|
+
import { AdminRoles } from "./roles.js";
|
|
10
|
+
import { AdminPermissions } from "./admin-permissions.js";
|
|
11
|
+
export { ADMIN_ROUTES, ADMIN_ROUTE_LABELS, AdminLicenceScreen, AdminMatrix, AdminMembers, AdminPermissions, AdminPermissionsProvider, AdminRoles, DirectRoles, EffectiveRoles, RoleAssignmentDialog, adminErrorMessage, effectiveRolesOf, sourceGroupOf, useAdminPermissions };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { AgentChatClient } from "../../agent-chat/types.js";
|
|
3
|
+
import { McpAppToolCaller } from "../../agent-chat/mcp-app/bridge.js";
|
|
4
|
+
import "react";
|
|
5
|
+
//#region src/components/app/agent-mount.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The agent pop-up, wired the way §19 says and not the way each extension
|
|
8
|
+
* remembered it.
|
|
9
|
+
*
|
|
10
|
+
* This module is loaded lazily by `CortenaApp`, and that is the whole reason
|
|
11
|
+
* it is a module at all: `AgentChatPopup` reaches `AgentChat`, which reaches
|
|
12
|
+
* `Markdown` and the A2UI renderer. An extension's first screen must not carry
|
|
13
|
+
* a markdown pipeline for a chat the user has not opened, so the frame imports
|
|
14
|
+
* this by `import()` and a bundler splits it off.
|
|
15
|
+
*
|
|
16
|
+
* `createClient` is passed in rather than imported for a different reason.
|
|
17
|
+
* `createAguiAgentChatClient` reaches `@ag-ui/client` — rxjs, zod 3, uuid,
|
|
18
|
+
* protobuf — and `test/bundle-probe.test.mjs` fails if `@ag-ui/*` is reachable
|
|
19
|
+
* from any entry but `cortena-ui/agent-chat`. Keeping the factory a prop keeps
|
|
20
|
+
* that guarantee exactly as it is, and still leaves the composition owning
|
|
21
|
+
* every argument the pop-up is built with: the user's token, read fresh on
|
|
22
|
+
* every request, and the agent slug. Both were the ones an extension shipped
|
|
23
|
+
* without.
|
|
24
|
+
*/
|
|
25
|
+
export interface CortenaAppAgent {
|
|
26
|
+
/** The AgentTemplate slug. Usually the extension id; §19.1. */
|
|
27
|
+
slug: string;
|
|
28
|
+
/** This extension's own gateway, never cortenacore. Default `/api/agent`. */
|
|
29
|
+
gatewayUrl?: string;
|
|
30
|
+
/**
|
|
31
|
+
* `createAguiAgentChatClient` from `cortena-ui/agent-chat`, passed through.
|
|
32
|
+
* One named import, and the composition supplies every argument.
|
|
33
|
+
*/
|
|
34
|
+
createClient: (options: {
|
|
35
|
+
baseUrl: string;
|
|
36
|
+
agentId: string;
|
|
37
|
+
getToken: () => string;
|
|
38
|
+
}) => AgentChatClient;
|
|
39
|
+
/**
|
|
40
|
+
* How a screen drawn inside the pop-up reaches this extension's own MCP
|
|
41
|
+
* server, with the user's credential (§19.2, step 4). Omitted, tool results
|
|
42
|
+
* stay ordinary cards.
|
|
43
|
+
*/
|
|
44
|
+
relay?: McpAppToolCaller;
|
|
45
|
+
/**
|
|
46
|
+
* `cortena-design/tokens.css?raw`. The sandboxed document has an opaque
|
|
47
|
+
* origin and `default-src 'none'`, so it can fetch nothing: the tokens go in
|
|
48
|
+
* as text or the screen renders unstyled.
|
|
49
|
+
*/
|
|
50
|
+
tokensCss?: string;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//# sourceMappingURL=agent-mount.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
import { AgentChatPopup } from "../agent-chat-popup.js";
|
|
4
|
+
import { renderMcpAppToolCall } from "../../agent-chat/mcp-app/render-tool-call.js";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
//#region src/components/app/agent-mount.tsx
|
|
8
|
+
function AgentMount({ extension, agent, getToken }) {
|
|
9
|
+
const identity = React.useRef({
|
|
10
|
+
agent,
|
|
11
|
+
getToken
|
|
12
|
+
});
|
|
13
|
+
identity.current = {
|
|
14
|
+
agent,
|
|
15
|
+
getToken
|
|
16
|
+
};
|
|
17
|
+
const client = React.useMemo(() => identity.current.agent.createClient({
|
|
18
|
+
baseUrl: identity.current.agent.gatewayUrl ?? "/api/agent",
|
|
19
|
+
agentId: identity.current.agent.slug,
|
|
20
|
+
getToken: () => identity.current.getToken()
|
|
21
|
+
}), []);
|
|
22
|
+
const renderToolCall = React.useMemo(() => {
|
|
23
|
+
const relay = identity.current.agent.relay;
|
|
24
|
+
if (!relay) return;
|
|
25
|
+
return renderMcpAppToolCall({
|
|
26
|
+
extensionId: extension.id,
|
|
27
|
+
callTool: relay,
|
|
28
|
+
tokensCss: identity.current.agent.tokensCss
|
|
29
|
+
});
|
|
30
|
+
}, [extension.id]);
|
|
31
|
+
return /* @__PURE__ */ jsx(AgentChatPopup, {
|
|
32
|
+
extension,
|
|
33
|
+
client,
|
|
34
|
+
renderToolCall
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
export { AgentMount as default };
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=agent-mount.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-mount.js","names":[],"sources":["../../../src/components/app/agent-mount.tsx"],"sourcesContent":["\"use client\";\n\nimport * as React from \"react\";\nimport { AgentChatPopup } from \"@/components/agent-chat-popup\";\nimport { renderMcpAppToolCall } from \"@/agent-chat/mcp-app/render-tool-call\";\nimport type { McpAppToolCaller } from \"@/agent-chat/mcp-app/bridge\";\nimport type { AgentChatClient } from \"@/agent-chat/types\";\n\n/**\n * The agent pop-up, wired the way §19 says and not the way each extension\n * remembered it.\n *\n * This module is loaded lazily by `CortenaApp`, and that is the whole reason\n * it is a module at all: `AgentChatPopup` reaches `AgentChat`, which reaches\n * `Markdown` and the A2UI renderer. An extension's first screen must not carry\n * a markdown pipeline for a chat the user has not opened, so the frame imports\n * this by `import()` and a bundler splits it off.\n *\n * `createClient` is passed in rather than imported for a different reason.\n * `createAguiAgentChatClient` reaches `@ag-ui/client` — rxjs, zod 3, uuid,\n * protobuf — and `test/bundle-probe.test.mjs` fails if `@ag-ui/*` is reachable\n * from any entry but `cortena-ui/agent-chat`. Keeping the factory a prop keeps\n * that guarantee exactly as it is, and still leaves the composition owning\n * every argument the pop-up is built with: the user's token, read fresh on\n * every request, and the agent slug. Both were the ones an extension shipped\n * without.\n */\n\nexport interface CortenaAppAgent {\n /** The AgentTemplate slug. Usually the extension id; §19.1. */\n slug: string;\n /** This extension's own gateway, never cortenacore. Default `/api/agent`. */\n gatewayUrl?: string;\n /**\n * `createAguiAgentChatClient` from `cortena-ui/agent-chat`, passed through.\n * One named import, and the composition supplies every argument.\n */\n createClient: (options: {\n baseUrl: string;\n agentId: string;\n getToken: () => string;\n }) => AgentChatClient;\n /**\n * How a screen drawn inside the pop-up reaches this extension's own MCP\n * server, with the user's credential (§19.2, step 4). Omitted, tool results\n * stay ordinary cards.\n */\n relay?: McpAppToolCaller;\n /**\n * `cortena-design/tokens.css?raw`. The sandboxed document has an opaque\n * origin and `default-src 'none'`, so it can fetch nothing: the tokens go in\n * as text or the screen renders unstyled.\n */\n tokensCss?: string;\n}\n\nexport interface AgentMountProps {\n extension: { id: string; name: string };\n agent: CortenaAppAgent;\n /** Read on every request rather than captured, so a renewal is picked up. */\n getToken: () => string;\n}\n\nfunction AgentMount({ extension, agent, getToken }: AgentMountProps) {\n // Built once for the life of the mount. The chat re-subscribes to the run\n // stream and refetches its session list whenever the client's identity\n // changes, so a client rebuilt per render is a session list refetched per\n // render. The identity is constant by construction: the slug is a string and\n // the token is read through a function rather than captured as a value.\n const identity = React.useRef({ agent, getToken });\n identity.current = { agent, getToken };\n\n const client = React.useMemo(\n () =>\n identity.current.agent.createClient({\n baseUrl: identity.current.agent.gatewayUrl ?? \"/api/agent\",\n agentId: identity.current.agent.slug,\n getToken: () => identity.current.getToken(),\n }),\n [],\n );\n\n const renderToolCall = React.useMemo(() => {\n const relay = identity.current.agent.relay;\n if (!relay) {\n return undefined;\n }\n return renderMcpAppToolCall({\n extensionId: extension.id,\n callTool: relay,\n tokensCss: identity.current.agent.tokensCss,\n // `onMessage` is left unset on purpose: the surface's own `send` takes\n // it, so a click inside a screen starts the next turn as the screen\n // rather than as a user who typed nothing (DESIGN-80).\n });\n // The extension id is fixed for the life of the app; rebuilding this would\n // reload a screen that is already running.\n }, [extension.id]);\n\n return <AgentChatPopup extension={extension} client={client} renderToolCall={renderToolCall} />;\n}\n\nexport default AgentMount;\n"],"mappings":";;;;;;;AA+DA,SAAS,WAAW,EAAE,WAAW,OAAO,YAA6B;CAMnE,MAAM,WAAW,MAAM,OAAO;EAAE;EAAO;CAAS,CAAC;CACjD,SAAS,UAAU;EAAE;EAAO;CAAS;CAErC,MAAM,SAAS,MAAM,cAEjB,SAAS,QAAQ,MAAM,aAAa;EAClC,SAAS,SAAS,QAAQ,MAAM,cAAc;EAC9C,SAAS,SAAS,QAAQ,MAAM;EAChC,gBAAgB,SAAS,QAAQ,SAAS;CAC5C,CAAC,GACH,CAAC,CACH;CAEA,MAAM,iBAAiB,MAAM,cAAc;EACzC,MAAM,QAAQ,SAAS,QAAQ,MAAM;EACrC,IAAI,CAAC,OACH;EAEF,OAAO,qBAAqB;GAC1B,aAAa,UAAU;GACvB,UAAU;GACV,WAAW,SAAS,QAAQ,MAAM;EAIpC,CAAC;CAGH,GAAG,CAAC,UAAU,EAAE,CAAC;CAEjB,OAAO,oBAAC,gBAAD;EAA2B;EAAmB;EAAwB;CAAiB,CAAA;AAChG"}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { AppShellHelp, AppShellMenuItem, AppShellNav, HelpPanelSlotProps } from "../app-shell.js";
|
|
3
|
+
import { LoginMethod, PasswordCredentials } from "../login-screen.js";
|
|
4
|
+
import { DialogContentProps } from "../dialog.js";
|
|
5
|
+
import { SessionPolicy, SessionSignOutReason, SessionTimers } from "../session-guard.js";
|
|
6
|
+
import { AdminRoute } from "../admin-permissions/types.js";
|
|
7
|
+
import { CortenaLaunch } from "./launch.js";
|
|
8
|
+
import { CortenaAppAgent } from "./agent-mount.js";
|
|
9
|
+
import { CortenaAppPermissions, CortenaAppUser, CortenaSettingsDeclaration } from "./settings.js";
|
|
10
|
+
import * as React from "react";
|
|
11
|
+
//#region src/components/app/cortena-app.d.ts
|
|
12
|
+
export interface CortenaAppExtension {
|
|
13
|
+
/** Registered mark id, from `x-cortena.icon` in the OpenAPI document (§10.1). */
|
|
14
|
+
id: string;
|
|
15
|
+
/** Display name. The shell's title, the tab's title and the login heading. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** Override the mark when it is not the extension id. Rarely needed. */
|
|
18
|
+
icon?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface CortenaAppAuth {
|
|
21
|
+
/** cortena-auth, in-cluster. From `config.json`; this component never calls it. */
|
|
22
|
+
authServiceUrl?: string;
|
|
23
|
+
/** cortena-auth's public origin — the account link on the Profile page. */
|
|
24
|
+
publicAuthUrl?: string;
|
|
25
|
+
orgId?: string;
|
|
26
|
+
/**
|
|
27
|
+
* What the org advertises, from `GET /api/orgs/:orgId/auth-info`. The fetch
|
|
28
|
+
* stays in the app: cortena-ui cannot depend on firebase, and a component
|
|
29
|
+
* that fetches cannot be rendered in the guide or in a test (§8.4).
|
|
30
|
+
*
|
|
31
|
+
* An empty or omitted list falls back to password rather than to a screen
|
|
32
|
+
* with no way in.
|
|
33
|
+
*/
|
|
34
|
+
methods?: readonly LoginMethod[];
|
|
35
|
+
onPassword?: (credentials: PasswordCredentials) => void;
|
|
36
|
+
onProvider?: (provider: Exclude<LoginMethod, "password">) => void;
|
|
37
|
+
/** A failed sign-in, in the user's words. */
|
|
38
|
+
error?: string;
|
|
39
|
+
loading?: boolean;
|
|
40
|
+
}
|
|
41
|
+
export interface CortenaSessionAdapter {
|
|
42
|
+
/**
|
|
43
|
+
* The launch, as the app read it synchronously before the first render —
|
|
44
|
+
* `readCortenaLaunch()`. A value rather than a callback, so this component
|
|
45
|
+
* touches no browser API while rendering (§7.3).
|
|
46
|
+
*/
|
|
47
|
+
launch: CortenaLaunch;
|
|
48
|
+
/** The current access token. Read on every use, never captured. */
|
|
49
|
+
getToken: () => string;
|
|
50
|
+
/**
|
|
51
|
+
* When the access token expires, for a token whose `exp` cannot be read — an
|
|
52
|
+
* opaque token, or one the decoder failed on. Epoch **milliseconds**; a value
|
|
53
|
+
* below `1e11` is read as epoch seconds instead. Exactly `SessionToken`'s
|
|
54
|
+
* own escape hatch, and the way to keep an unreadable credential guarded
|
|
55
|
+
* rather than landing on the signed-out screen (see the note on the guard
|
|
56
|
+
* below).
|
|
57
|
+
*/
|
|
58
|
+
expiresAt?: number;
|
|
59
|
+
/**
|
|
60
|
+
* The origins a host declaration and a host-pushed token are accepted from —
|
|
61
|
+
* CortenaWeb's, from this extension's own configuration, never from the
|
|
62
|
+
* request.
|
|
63
|
+
*
|
|
64
|
+
* Given, the composition listens for the host's first bridge message and
|
|
65
|
+
* takes the declaration from it; `event.origin` is the browser's, not the
|
|
66
|
+
* sender's, and is the only part of a `postMessage` that cannot be forged
|
|
67
|
+
* from inside the page that sent it, which makes this the safer of the two
|
|
68
|
+
* declaration paths (see `launch.ts` on what `ancestorOrigins` cannot
|
|
69
|
+
* promise). With none given nothing is ever trusted.
|
|
70
|
+
*/
|
|
71
|
+
hostOrigins?: readonly string[];
|
|
72
|
+
/** `POST /api/auth/refresh`. Only called when this app can refresh. */
|
|
73
|
+
refresh?: () => Promise<{
|
|
74
|
+
accessToken: string;
|
|
75
|
+
}>;
|
|
76
|
+
/**
|
|
77
|
+
* `POST /api/auth/logout`, or the message that asks the host to sign out.
|
|
78
|
+
*
|
|
79
|
+
* Omit it in a framed launch where there is no host sign-out message to
|
|
80
|
+
* send: the avatar menu's Log out is then disabled, which is what every
|
|
81
|
+
* shell swap does today and is honest — a menu item that silently does
|
|
82
|
+
* nothing is worse than one that is visibly unavailable.
|
|
83
|
+
*/
|
|
84
|
+
signOut?: () => Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Renewed tokens, from wherever the app gets them — the bridge, its own
|
|
87
|
+
* store, a rotation it ran itself. An alternative to `hostOrigins` for an
|
|
88
|
+
* app that would rather own the `message` listener; both end up in the same
|
|
89
|
+
* place, and a token identical to the one in force costs a comparison.
|
|
90
|
+
*/
|
|
91
|
+
onTokenRenewed?: (listener: (accessToken: string) => void) => () => void;
|
|
92
|
+
/** Org policy from `GET /api/orgs/:orgId/session-policy` (§10.4.2). */
|
|
93
|
+
policy?: Partial<SessionPolicy>;
|
|
94
|
+
/** The `BroadcastChannel` name. One per origin unless two apps must not share. */
|
|
95
|
+
channelName?: string;
|
|
96
|
+
/** The clock and the timers. Injected by tests and by the guide. */
|
|
97
|
+
now?: () => number;
|
|
98
|
+
timers?: SessionTimers;
|
|
99
|
+
}
|
|
100
|
+
/** Where the standard pages live. Overridable; the defaults are the fleet's. */
|
|
101
|
+
export interface CortenaAppRoutes {
|
|
102
|
+
/** @default "/me" */
|
|
103
|
+
profile?: string;
|
|
104
|
+
/** @default "/settings" */
|
|
105
|
+
settings?: string;
|
|
106
|
+
/** @default "/settings/permissions" */
|
|
107
|
+
permissions?: string;
|
|
108
|
+
}
|
|
109
|
+
export declare const CORTENA_STANDARD_ROUTES: Required<CortenaAppRoutes>;
|
|
110
|
+
export interface CortenaAppProps {
|
|
111
|
+
extension: CortenaAppExtension;
|
|
112
|
+
user: CortenaAppUser;
|
|
113
|
+
auth?: CortenaAppAuth;
|
|
114
|
+
session: CortenaSessionAdapter;
|
|
115
|
+
/**
|
|
116
|
+
* `extension` mounts the agent pop-up and is the default. `host` is
|
|
117
|
+
* CortenaWeb: the pop-up slot stays empty because the host *is* the chat,
|
|
118
|
+
* and its own guard is the one that renews and pushes tokens into frames.
|
|
119
|
+
*/
|
|
120
|
+
role?: "extension" | "host";
|
|
121
|
+
nav?: AppShellNav | null;
|
|
122
|
+
agent?: CortenaAppAgent;
|
|
123
|
+
settings?: CortenaSettingsDeclaration;
|
|
124
|
+
permissions?: CortenaAppPermissions;
|
|
125
|
+
help?: AppShellHelp;
|
|
126
|
+
helpPanel?: React.ReactNode | ((props: HelpPanelSlotProps) => React.ReactNode);
|
|
127
|
+
headerSlot?: React.ReactNode;
|
|
128
|
+
/** Extra avatar-menu items, between Settings and Log out. */
|
|
129
|
+
menuItems?: AppShellMenuItem[];
|
|
130
|
+
bottomInset?: string | number;
|
|
131
|
+
themeStorageKey?: string;
|
|
132
|
+
/** The current path. The composition compares it to the standard routes. */
|
|
133
|
+
pathname: string;
|
|
134
|
+
/** Take over a navigation. The composition never touches `history`. */
|
|
135
|
+
onNavigate: (href: string) => void;
|
|
136
|
+
/**
|
|
137
|
+
* The page half of the tab title for a route: `documentTitle(name, page)`
|
|
138
|
+
* gives "<page> · <name> · Cortena" (§10.5, P-38). Set after mount, never
|
|
139
|
+
* during a render.
|
|
140
|
+
*
|
|
141
|
+
* `false` leaves the title alone entirely, for a surface that is not the
|
|
142
|
+
* whole tab — the guide, which mounts four of these on one page, and a host
|
|
143
|
+
* that already sets its own. The composition does not assume it owns the
|
|
144
|
+
* document; it assumes it usually does.
|
|
145
|
+
*/
|
|
146
|
+
documentTitle?: ((pathname: string) => string | undefined) | false;
|
|
147
|
+
routes?: CortenaAppRoutes;
|
|
148
|
+
/**
|
|
149
|
+
* Non-modal, and portalled somewhere other than the body. For the guide,
|
|
150
|
+
* where three theme frames must not fight over focus. The product leaves
|
|
151
|
+
* this alone, exactly as it leaves `SessionGuard`'s own pair alone.
|
|
152
|
+
*/
|
|
153
|
+
sessionDialog?: {
|
|
154
|
+
modal?: boolean;
|
|
155
|
+
container?: DialogContentProps["container"];
|
|
156
|
+
};
|
|
157
|
+
/** Told when the session ends, so the app can clear its own stores. */
|
|
158
|
+
onSignedOut?: (reason: SessionSignOutReason) => void;
|
|
159
|
+
/**
|
|
160
|
+
* On the frame's own root — the shell, or the sign-in screen when there is
|
|
161
|
+
* no session. `min-h-0 h-full` is how the guide boxes it.
|
|
162
|
+
*/
|
|
163
|
+
className?: string;
|
|
164
|
+
/** The routed content. Replaced by a standard page when `pathname` names one. */
|
|
165
|
+
children?: React.ReactNode;
|
|
166
|
+
}
|
|
167
|
+
export type CortenaStandardRoute = {
|
|
168
|
+
kind: "profile";
|
|
169
|
+
} | {
|
|
170
|
+
kind: "settings";
|
|
171
|
+
adminRoute?: AdminRoute;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* Which standard page a path names, or `undefined` for the app's own content.
|
|
175
|
+
*
|
|
176
|
+
* Exported because an app that would rather place these pages itself — a Next
|
|
177
|
+
* route segment, a react-router `<Route>` — needs the same answer this
|
|
178
|
+
* composition uses, rather than a second copy of the rule.
|
|
179
|
+
*/
|
|
180
|
+
export declare function matchCortenaStandardRoute(pathname: string, routes?: CortenaAppRoutes): CortenaStandardRoute | undefined;
|
|
181
|
+
declare function CortenaApp({ extension, user, auth, session, role, nav, agent, settings, permissions, help, helpPanel, headerSlot, menuItems, bottomInset, themeStorageKey, pathname, onNavigate, documentTitle: pageTitle, routes, sessionDialog, onSignedOut, className, children }: CortenaAppProps): React.JSX.Element;
|
|
182
|
+
//#endregion
|
|
183
|
+
export { CortenaApp };
|
|
184
|
+
//# sourceMappingURL=cortena-app.d.ts.map
|