@struxa/extension-sdk 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +243 -0
- package/package.json +1 -1
- package/src/client.ts +28 -0
- package/src/index.ts +57 -1
- package/src/server.ts +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# @struxa/extension-sdk
|
|
2
|
+
|
|
3
|
+
SDK for building [Struxa](https://struxa.cloud) extensions. Provides manifest types and validation, the server-side `defineExtension()` helper, a preconfigured oRPC client, the iframe host bridge, and React hooks — everything an extension needs to integrate with the host panel.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @struxa/extension-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Exports
|
|
10
|
+
|
|
11
|
+
| Entry point | Contents |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `@struxa/extension-sdk` | Manifest schema/types, permission constants, `HOST_API_VERSION` |
|
|
14
|
+
| `@struxa/extension-sdk/server` | `defineExtension()`, `ExtensionContext`, hook typings |
|
|
15
|
+
| `@struxa/extension-sdk/client` | `createHostClient()`, `createBridge()`, host bridge types |
|
|
16
|
+
| `@struxa/extension-sdk/react` | `createExtension()`, `useHostBridge()` |
|
|
17
|
+
|
|
18
|
+
## Server entry (`server/index.js`)
|
|
19
|
+
|
|
20
|
+
The host loads `server/index.js`, calls `mod.default.register(ctx)`, and mounts the returned router at `ext.<id>.*`. Use `defineExtension()` as the default export:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
import { defineExtension } from "@struxa/extension-sdk/server";
|
|
24
|
+
|
|
25
|
+
export default defineExtension({
|
|
26
|
+
async register(ctx) {
|
|
27
|
+
ctx.hooks.on("server.created", async ({ serverId }) => {
|
|
28
|
+
ctx.logger.info("server created", { serverId });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const router = {
|
|
32
|
+
ping: ctx.procedures.protectedProcedure.handler(() => ({
|
|
33
|
+
ok: true,
|
|
34
|
+
timestamp: new Date().toISOString(),
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return { router };
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Context (`ctx`)
|
|
44
|
+
|
|
45
|
+
All host capabilities arrive through `ctx` — there is no other sanctioned import path from `server/index.js`:
|
|
46
|
+
|
|
47
|
+
| Field | Type | Requires permission |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `ctx.logger` | `{ info, warn, error }` | — |
|
|
50
|
+
| `ctx.db` | Drizzle handle (fenced to `ext_<id>_*` tables) | `db:own` |
|
|
51
|
+
| `ctx.coreMeta.<entity>` | `{ get, set }` on the entity's `metadata` column | `core.metadata:<entity>` |
|
|
52
|
+
| `ctx.settings` | `{ get, set, all }` scoped to approved key prefixes | `settings:<prefix>` |
|
|
53
|
+
| `ctx.hooks` | `{ on(event, handler) }` | `hook:<event>` |
|
|
54
|
+
| `ctx.procedures` | `{ publicProcedure?, protectedProcedure?, adminProcedure? }` | `api:<level>` |
|
|
55
|
+
| `ctx.fieldOutputs` | `{ set, clear }` — publish computed values for named UI fields | `output:<entity>:<field>` |
|
|
56
|
+
|
|
57
|
+
### Hook events
|
|
58
|
+
|
|
59
|
+
Subscribe via `ctx.hooks.on(event, handler)` with the matching `hook:<event>` permission:
|
|
60
|
+
|
|
61
|
+
| Event | Payload | Description |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `server.created` | `{ serverId, userId }` | A server was created |
|
|
64
|
+
| `server.updated` | `{ serverId, userId }` | A server's config was updated |
|
|
65
|
+
| `server.deleted` | `{ serverId, userId }` | A server was deleted |
|
|
66
|
+
| `server.power` | `{ serverId, action }` | A power action was sent to a server |
|
|
67
|
+
| `server.settings.saved` | `{ serverId, key, value }` | One of this extension's server settings was saved by the user |
|
|
68
|
+
| `admin.settings.saved` | `{ tabId, changes }` | One of this extension's admin settings tabs was saved |
|
|
69
|
+
| `node.created` | `{ nodeId }` | A node was created |
|
|
70
|
+
| `node.updated` | `{ nodeId }` | A node was updated |
|
|
71
|
+
| `node.deleted` | `{ nodeId }` | A node was deleted |
|
|
72
|
+
| `user.created` | `{ userId }` | A user registered |
|
|
73
|
+
| `user.updated` | `{ userId }` | A user's profile was updated |
|
|
74
|
+
| `user.deleted` | `{ userId }` | A user was deleted |
|
|
75
|
+
|
|
76
|
+
`server.settings.saved` and `admin.settings.saved` are fired only to the extension whose settings were changed — other extensions do not receive these events.
|
|
77
|
+
|
|
78
|
+
### `ctx.fieldOutputs` — requires `output:<entity>:<field>`
|
|
79
|
+
|
|
80
|
+
Publish a computed value for a named UI field. The host panel reads these values and displays them instead of the default (e.g. showing a Cloudflare-generated subdomain instead of the raw server IP).
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
// In register(), populate on startup from persisted data:
|
|
84
|
+
ctx.hooks.on("server.created", async ({ serverId }) => {
|
|
85
|
+
const subdomain = await provisionSubdomain(serverId);
|
|
86
|
+
ctx.fieldOutputs.set("server", "address", serverId, `${subdomain}:25565`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Clear when the server is deleted:
|
|
90
|
+
ctx.hooks.on("server.deleted", async ({ serverId }) => {
|
|
91
|
+
ctx.fieldOutputs.clear("server", "address", serverId);
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Values are in-memory — repopulate them in `register()` by reading from `ctx.settings` or `ctx.coreMeta` if you need them to survive restarts.
|
|
96
|
+
|
|
97
|
+
Currently overridable fields:
|
|
98
|
+
|
|
99
|
+
| Entity | Field | Where it appears |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `server` | `address` | Server console page — address stat |
|
|
102
|
+
| `server` | `sftp.host` | Server settings page — SFTP section and sidebar |
|
|
103
|
+
|
|
104
|
+
## React UI
|
|
105
|
+
|
|
106
|
+
### `createExtension(element)`
|
|
107
|
+
|
|
108
|
+
Bootstraps a React app into `#root`. Wraps in an error boundary and removes the boot-status indicator.
|
|
109
|
+
|
|
110
|
+
```tsx
|
|
111
|
+
// src/main.tsx
|
|
112
|
+
import { createExtension } from "@struxa/extension-sdk/react";
|
|
113
|
+
import { App } from "./App";
|
|
114
|
+
|
|
115
|
+
createExtension(<App />);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### `useHostBridge()`
|
|
119
|
+
|
|
120
|
+
Mounts the iframe bridge once, exposes the host context (theme, session, route params, translated messages), and returns navigation/toast/resize helpers. Auto-resize is on by default.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
function App() {
|
|
124
|
+
const { context, navigate, toast } = useHostBridge();
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<button onClick={() => toast("success", "done!")}>
|
|
128
|
+
Hello, {context?.session.userId}
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `bridge.t(key, params?)` — translations
|
|
135
|
+
|
|
136
|
+
The bridge exposes a `t()` helper that resolves dot-notation keys from the extension's translated messages (pushed by the host on init from the `messages` manifest field):
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const bridge = createBridge({ onInit: (ctx) => render(ctx) });
|
|
140
|
+
|
|
141
|
+
// After onInit fires:
|
|
142
|
+
bridge.t("nav.title") // → "My Extension"
|
|
143
|
+
bridge.t("msg.count", { n: 3 }) // → "3 items" (if messages has "msg.count": "{n} items")
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Falls back to the key itself if no translation is found. Supports `{param}` placeholder substitution.
|
|
147
|
+
|
|
148
|
+
## oRPC client (`createHostClient`)
|
|
149
|
+
|
|
150
|
+
Same-origin, cookie-authenticated. Call your extension's procedures under `client.ext["<your-id>"].*`:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { createHostClient } from "@struxa/extension-sdk/client";
|
|
154
|
+
|
|
155
|
+
const client = createHostClient();
|
|
156
|
+
const res = await client.ext["hello-world"].ping();
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Manifest (`manifest.json`)
|
|
160
|
+
|
|
161
|
+
Validated by `manifestSchema` (Zod). Full example:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"id": "hello-world",
|
|
166
|
+
"name": "Hello World",
|
|
167
|
+
"version": "1.0.0",
|
|
168
|
+
"struxaApi": "^1.0.0",
|
|
169
|
+
"permissions": [
|
|
170
|
+
"hook:server.created",
|
|
171
|
+
"api:protected",
|
|
172
|
+
"core.metadata:server",
|
|
173
|
+
"output:server:address",
|
|
174
|
+
"output:server:sftp.host"
|
|
175
|
+
],
|
|
176
|
+
"messages": {
|
|
177
|
+
"en": "messages/en.json",
|
|
178
|
+
"pl": "messages/pl.json"
|
|
179
|
+
},
|
|
180
|
+
"ui": {
|
|
181
|
+
"pages": [
|
|
182
|
+
{ "route": "/hello", "section": "panel", "label": "nav.hello", "icon": "Blocks" }
|
|
183
|
+
],
|
|
184
|
+
"slots": [
|
|
185
|
+
{ "slot": "server.stats.after", "widget": "/hello/stats-widget" }
|
|
186
|
+
],
|
|
187
|
+
"serverSettings": [
|
|
188
|
+
{
|
|
189
|
+
"key": "subdomain_prefix",
|
|
190
|
+
"label": "Subdomain prefix",
|
|
191
|
+
"description": "Prefix used when generating Cloudflare subdomains.",
|
|
192
|
+
"type": "text",
|
|
193
|
+
"placeholder": "mc"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"key": "enabled",
|
|
197
|
+
"label": "Enable automatic subdomains",
|
|
198
|
+
"type": "toggle"
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
"adminSettingsTabs": [
|
|
202
|
+
{
|
|
203
|
+
"id": "cloudflare",
|
|
204
|
+
"label": "Cloudflare",
|
|
205
|
+
"sections": [
|
|
206
|
+
{
|
|
207
|
+
"title": "API Credentials",
|
|
208
|
+
"description": "Connect your Cloudflare account.",
|
|
209
|
+
"fields": [
|
|
210
|
+
{ "key": "api_token", "label": "API Token", "type": "password" },
|
|
211
|
+
{ "key": "zone_id", "label": "Zone ID", "type": "text", "placeholder": "abc123…" },
|
|
212
|
+
{
|
|
213
|
+
"key": "plan",
|
|
214
|
+
"label": "Plan",
|
|
215
|
+
"type": "select",
|
|
216
|
+
"options": [
|
|
217
|
+
{ "value": "free", "label": "Free" },
|
|
218
|
+
{ "value": "pro", "label": "Pro" }
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Permission grammar
|
|
231
|
+
|
|
232
|
+
| Permission | Grants |
|
|
233
|
+
|---|---|
|
|
234
|
+
| `db:own` | Create and use `ext_<id>_*` tables |
|
|
235
|
+
| `core.metadata:<entity>` | Read/write the `metadata` JSON on `server`, `node`, or `user` |
|
|
236
|
+
| `settings:<prefix>` | Get/set settings keys under `<prefix>` |
|
|
237
|
+
| `hook:<event>` | Subscribe to a lifecycle event (e.g. `hook:server.created`) |
|
|
238
|
+
| `api:public` / `api:protected` / `api:admin` | Expose oRPC procedures at that auth level |
|
|
239
|
+
| `output:<entity>:<field>` | Publish a computed value for a named UI field (e.g. `output:server:address`) |
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
[MIT](https://github.com/struxadotcloud/struxa/blob/main/LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@struxa/extension-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SDK for building Struxa extensions: manifest types, server entry helpers, the iframe host bridge, a preconfigured oRPC client, and React hooks.",
|
|
6
6
|
"license": "MIT",
|
package/src/client.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface HostContext {
|
|
|
49
49
|
session: HostSessionSummary;
|
|
50
50
|
/** Route context the iframe was opened with (e.g. { serverId }). */
|
|
51
51
|
params: Record<string, string>;
|
|
52
|
+
/** Translated messages for this extension's locale, keyed by dot-notation path. */
|
|
53
|
+
messages: Record<string, unknown>;
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
type HostInbound =
|
|
@@ -88,6 +90,11 @@ export interface Bridge {
|
|
|
88
90
|
navigate(route: string): void;
|
|
89
91
|
toast(level: "success" | "error" | "info", message: string): void;
|
|
90
92
|
reportHeight(height?: number): void;
|
|
93
|
+
/**
|
|
94
|
+
* Resolve a dot-notation key from the extension's messages (e.g. `"nav.title"`).
|
|
95
|
+
* Supports `{param}` placeholder substitution. Falls back to the key itself.
|
|
96
|
+
*/
|
|
97
|
+
t(key: string, params?: Record<string, string | number>): string;
|
|
91
98
|
destroy(): void;
|
|
92
99
|
}
|
|
93
100
|
|
|
@@ -100,6 +107,17 @@ function applyTheme(theme: HostTheme): void {
|
|
|
100
107
|
}
|
|
101
108
|
}
|
|
102
109
|
|
|
110
|
+
/** Resolve a dotted key path into a nested object, returning a string or null. */
|
|
111
|
+
function resolveDotKey(obj: Record<string, unknown>, key: string): string | null {
|
|
112
|
+
const parts = key.split(".");
|
|
113
|
+
let cur: unknown = obj;
|
|
114
|
+
for (const part of parts) {
|
|
115
|
+
if (cur == null || typeof cur !== "object") return null;
|
|
116
|
+
cur = (cur as Record<string, unknown>)[part];
|
|
117
|
+
}
|
|
118
|
+
return typeof cur === "string" ? cur : null;
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
/**
|
|
104
122
|
* Initialize the bridge. Call once on iframe boot.
|
|
105
123
|
*/
|
|
@@ -118,6 +136,16 @@ export function createBridge(options: BridgeOptions = {}): Bridge {
|
|
|
118
136
|
? document.documentElement.scrollHeight
|
|
119
137
|
: 0),
|
|
120
138
|
}),
|
|
139
|
+
t(key, params) {
|
|
140
|
+
const messages = bridge.context?.messages ?? {};
|
|
141
|
+
let str = resolveDotKey(messages, key) ?? key;
|
|
142
|
+
if (params) {
|
|
143
|
+
for (const [k, v] of Object.entries(params)) {
|
|
144
|
+
str = str.replaceAll(`{${k}}`, String(v));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return str;
|
|
148
|
+
},
|
|
121
149
|
destroy: () => {
|
|
122
150
|
if (typeof window !== "undefined") {
|
|
123
151
|
window.removeEventListener("message", onMessage);
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ export const HOOK_EVENTS = [
|
|
|
19
19
|
"server.updated",
|
|
20
20
|
"server.deleted",
|
|
21
21
|
"server.power",
|
|
22
|
+
"server.settings.saved",
|
|
23
|
+
"admin.settings.saved",
|
|
22
24
|
"node.created",
|
|
23
25
|
"node.updated",
|
|
24
26
|
"node.deleted",
|
|
@@ -43,7 +45,7 @@ export type ApiLevel = "public" | "protected" | "admin";
|
|
|
43
45
|
const permissionSchema = z
|
|
44
46
|
.string()
|
|
45
47
|
.regex(
|
|
46
|
-
/^(db:own|core\.metadata:(server|node|user)|settings:[a-zA-Z0-9_.*-]+|hook:[a-zA-Z0-9_.*-]+|api:(public|protected|admin))$/,
|
|
48
|
+
/^(db:own|core\.metadata:(server|node|user)|settings:[a-zA-Z0-9_.*-]+|hook:[a-zA-Z0-9_.*-]+|api:(public|protected|admin)|output:[a-z]+:[a-zA-Z0-9_]+)$/,
|
|
47
49
|
"invalid permission",
|
|
48
50
|
);
|
|
49
51
|
|
|
@@ -74,6 +76,45 @@ const uiSlotSchema = z.object({
|
|
|
74
76
|
widget: z.string().min(1),
|
|
75
77
|
});
|
|
76
78
|
|
|
79
|
+
const serverSettingFieldSchema = z.object({
|
|
80
|
+
/** Storage key — alphanumeric + underscores only, unique within this extension. */
|
|
81
|
+
key: z.string().regex(/^[a-zA-Z0-9_]+$/).min(1).max(64),
|
|
82
|
+
/** Label shown in the settings row. */
|
|
83
|
+
label: z.string().min(1).max(120),
|
|
84
|
+
description: z.string().max(500).optional(),
|
|
85
|
+
/** Rendered as a text input or an on/off toggle. */
|
|
86
|
+
type: z.enum(["text", "toggle"]),
|
|
87
|
+
/** Placeholder text for text inputs. */
|
|
88
|
+
placeholder: z.string().max(255).optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const adminSettingsFieldSchema = z.object({
|
|
92
|
+
/** Storage key — alphanumeric + underscores only. Must be unique within the tab. */
|
|
93
|
+
key: z.string().regex(/^[a-zA-Z0-9_]+$/).min(1).max(64),
|
|
94
|
+
label: z.string().min(1).max(120),
|
|
95
|
+
description: z.string().max(500).optional(),
|
|
96
|
+
type: z.enum(["text", "password", "textarea", "toggle", "select"]),
|
|
97
|
+
placeholder: z.string().max(255).optional(),
|
|
98
|
+
/** Required when type is "select". */
|
|
99
|
+
options: z
|
|
100
|
+
.array(z.object({ value: z.string().min(1).max(255), label: z.string().min(1).max(120) }))
|
|
101
|
+
.optional(),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const adminSettingsSectionSchema = z.object({
|
|
105
|
+
title: z.string().min(1).max(120),
|
|
106
|
+
description: z.string().max(500).optional(),
|
|
107
|
+
fields: z.array(adminSettingsFieldSchema).min(1),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const adminSettingsTabSchema = z.object({
|
|
111
|
+
/** Unique within this extension — used as part of the tab key. */
|
|
112
|
+
id: z.string().regex(/^[a-zA-Z0-9_-]+$/).min(1).max(64),
|
|
113
|
+
/** Display label for the tab. May be a literal string or an i18n key resolved under ext.<id>.*. */
|
|
114
|
+
label: z.string().min(1).max(120),
|
|
115
|
+
sections: z.array(adminSettingsSectionSchema).min(1),
|
|
116
|
+
});
|
|
117
|
+
|
|
77
118
|
export const manifestSchema = z.object({
|
|
78
119
|
id: z
|
|
79
120
|
.string()
|
|
@@ -89,6 +130,18 @@ export const manifestSchema = z.object({
|
|
|
89
130
|
.object({
|
|
90
131
|
pages: z.array(uiPageSchema).default([]),
|
|
91
132
|
slots: z.array(uiSlotSchema).default([]),
|
|
133
|
+
/**
|
|
134
|
+
* Custom settings rows injected into the server settings page.
|
|
135
|
+
* Requires the `core.metadata:server` permission to be granted —
|
|
136
|
+
* values are stored in the server's metadata under this extension's key.
|
|
137
|
+
*/
|
|
138
|
+
serverSettings: z.array(serverSettingFieldSchema).default([]),
|
|
139
|
+
/**
|
|
140
|
+
* Tabs injected into the admin /admin/settings page. Values are stored
|
|
141
|
+
* in the global settings table under the extension's namespace and
|
|
142
|
+
* accessible at runtime via `ctx.settings`.
|
|
143
|
+
*/
|
|
144
|
+
adminSettingsTabs: z.array(adminSettingsTabSchema).default([]),
|
|
92
145
|
})
|
|
93
146
|
.optional(),
|
|
94
147
|
/** Map of locale -> relative path of a namespaced messages JSON file. */
|
|
@@ -98,6 +151,9 @@ export const manifestSchema = z.object({
|
|
|
98
151
|
export type Manifest = z.infer<typeof manifestSchema>;
|
|
99
152
|
export type ManifestUiPage = z.infer<typeof uiPageSchema>;
|
|
100
153
|
export type ManifestUiSlot = z.infer<typeof uiSlotSchema>;
|
|
154
|
+
export type ManifestServerSettingField = z.infer<typeof serverSettingFieldSchema>;
|
|
155
|
+
export type ManifestAdminSettingsField = z.infer<typeof adminSettingsFieldSchema>;
|
|
156
|
+
export type ManifestAdminSettingsTab = z.infer<typeof adminSettingsTabSchema>;
|
|
101
157
|
|
|
102
158
|
/** The current host API version. Extensions declare a range against this. */
|
|
103
159
|
export const HOST_API_VERSION = "1.0.0";
|
package/src/server.ts
CHANGED
|
@@ -51,6 +51,8 @@ export interface HookPayloads {
|
|
|
51
51
|
"server.updated": { serverId: string; userId: string | null };
|
|
52
52
|
"server.deleted": { serverId: string; userId: string | null };
|
|
53
53
|
"server.power": { serverId: string; action: string };
|
|
54
|
+
"server.settings.saved": { serverId: string; key: string; value: string };
|
|
55
|
+
"admin.settings.saved": { tabId: string; changes: Record<string, string> };
|
|
54
56
|
"node.created": { nodeId: string };
|
|
55
57
|
"node.updated": { nodeId: string };
|
|
56
58
|
"node.deleted": { nodeId: string };
|
|
@@ -81,6 +83,16 @@ export interface ScopedProcedures {
|
|
|
81
83
|
adminProcedure?: ProcedureBuilder;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Publish computed values for named output fields (e.g. `server.address`).
|
|
88
|
+
* Granted by `output:<entity>:<field>` permissions. Values are held in memory
|
|
89
|
+
* and should be repopulated in `register()` if the extension persists them.
|
|
90
|
+
*/
|
|
91
|
+
export interface FieldOutputs {
|
|
92
|
+
set(entity: string, field: string, entityId: string, value: string): void;
|
|
93
|
+
clear(entity: string, field: string, entityId: string): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
84
96
|
export interface ExtensionContext {
|
|
85
97
|
readonly extId: string;
|
|
86
98
|
readonly version: string;
|
|
@@ -95,6 +107,7 @@ export interface ExtensionContext {
|
|
|
95
107
|
readonly settings: ScopedSettings;
|
|
96
108
|
readonly hooks: HookApi;
|
|
97
109
|
readonly procedures: ScopedProcedures;
|
|
110
|
+
readonly fieldOutputs: FieldOutputs;
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
export interface ExtensionRegistration {
|