@bardioc/app-sdk 1.0.1 → 1.0.3
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 +2 -0
- package/dist/api/request.d.ts +4 -4
- package/dist/client.d.ts +4 -0
- package/dist/client.js +4 -0
- package/dist/contract-matrix.d.ts +148 -0
- package/dist/contract-matrix.js +150 -0
- package/dist/contract.d.ts +55 -16
- package/dist/contract.js +6 -0
- package/dist/dev-auth-proxy-core.d.ts +24 -0
- package/dist/dev-auth-proxy-core.js +59 -0
- package/dist/dev-auth-vite.d.ts +34 -0
- package/dist/dev-auth-vite.js +221 -0
- package/dist/dev-idb.d.ts +4 -4
- package/dist/dev-session-cli.d.ts +34 -0
- package/dist/dev-session-cli.js +125 -0
- package/dist/dev.js +12 -0
- package/dist/host-bridge.d.ts +3 -0
- package/dist/host-bridge.js +318 -0
- package/dist/index.d.ts +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +14 -2
- package/dist/transports/backend-transport.d.ts +24 -0
- package/dist/transports/backend-transport.js +54 -0
- package/dist/transports/graph-transport.d.ts +241 -0
- package/dist/transports/graph-transport.js +616 -0
- package/dist/transports/os-transport.d.ts +190 -0
- package/dist/transports/os-transport.js +444 -0
- package/dist/vite.js +25 -11
- package/package.json +4 -4
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { SDK_MSG } from './protocol.js';
|
|
2
|
+
import { BackendTransport } from './transports/backend-transport.js';
|
|
3
|
+
import { GraphTransport } from './transports/graph-transport.js';
|
|
4
|
+
import { OsTransport } from './transports/os-transport.js';
|
|
5
|
+
let counter = 0;
|
|
6
|
+
function nextId() {
|
|
7
|
+
return `sdk_${Date.now()}_${++counter}`;
|
|
8
|
+
}
|
|
9
|
+
function assertStoreName(storeName) {
|
|
10
|
+
const normalized = storeName.trim();
|
|
11
|
+
if (!normalized) {
|
|
12
|
+
throw new Error('storeName must be a non-empty string');
|
|
13
|
+
}
|
|
14
|
+
return normalized;
|
|
15
|
+
}
|
|
16
|
+
/** Create a bridge between an app and its host OS, over `postMessage` unless `config.channel` says otherwise. */
|
|
17
|
+
export function createHostBridge(config) {
|
|
18
|
+
const { appId: hintAppId, connectTimeout = 5000, debug = false } = config;
|
|
19
|
+
function log(...args) {
|
|
20
|
+
if (debug)
|
|
21
|
+
console.log('[app-sdk]', ...args);
|
|
22
|
+
}
|
|
23
|
+
function windowChannel() {
|
|
24
|
+
const hostWindow = globalThis.window;
|
|
25
|
+
// When inside an iframe, document.referrer gives us the parent's origin.
|
|
26
|
+
const inferredHostOrigin = (() => {
|
|
27
|
+
try {
|
|
28
|
+
if (hostWindow.parent !== hostWindow && document.referrer) {
|
|
29
|
+
return new URL(document.referrer).origin;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// ignore parse errors
|
|
34
|
+
}
|
|
35
|
+
return hostWindow.location.origin;
|
|
36
|
+
})();
|
|
37
|
+
const targetOrigin = config.targetOrigin ?? inferredHostOrigin;
|
|
38
|
+
const isInsideIframe = hostWindow.parent !== hostWindow;
|
|
39
|
+
log('createHostBridge', { appId: hintAppId, targetOrigin, isInsideIframe });
|
|
40
|
+
function isAllowedMessageOrigin(origin) {
|
|
41
|
+
return origin === targetOrigin || origin === hostWindow.location.origin;
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
post: message => hostWindow.parent.postMessage(message, targetOrigin),
|
|
45
|
+
subscribe(receive) {
|
|
46
|
+
const onMessage = (event) => {
|
|
47
|
+
if (!isAllowedMessageOrigin(event.origin)) {
|
|
48
|
+
log('ignoring message from', event.origin, '(expected', targetOrigin, ')');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
receive(event.data);
|
|
52
|
+
};
|
|
53
|
+
hostWindow.addEventListener('message', onMessage);
|
|
54
|
+
return () => hostWindow.removeEventListener('message', onMessage);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const channel = config.channel ?? windowChannel();
|
|
59
|
+
let _connected = false;
|
|
60
|
+
let _context = null;
|
|
61
|
+
let _destroyed = false;
|
|
62
|
+
const commandHandlers = new Map();
|
|
63
|
+
const osConfigUpdateCallbacks = new Set();
|
|
64
|
+
const eventHandlers = new Map();
|
|
65
|
+
const pending = new Map();
|
|
66
|
+
function handleCommandInvoke(data) {
|
|
67
|
+
const { commandId } = data.payload;
|
|
68
|
+
const handler = commandHandlers.get(commandId);
|
|
69
|
+
if (!handler) {
|
|
70
|
+
respondToHost(data.id, null, `Command "${commandId}" is not registered`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
Promise.resolve()
|
|
74
|
+
.then(() => handler())
|
|
75
|
+
.then(() => {
|
|
76
|
+
respondToHost(data.id, { ok: true });
|
|
77
|
+
})
|
|
78
|
+
.catch((error) => {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
respondToHost(data.id, null, message);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function handleOsConfigUpdate(data) {
|
|
84
|
+
const newOsConfig = data.payload;
|
|
85
|
+
if (_context) {
|
|
86
|
+
_context.osConfig = newOsConfig;
|
|
87
|
+
}
|
|
88
|
+
for (const callback of osConfigUpdateCallbacks) {
|
|
89
|
+
try {
|
|
90
|
+
callback(newOsConfig);
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error('[app-sdk] OS config update callback error:', error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function handleGraphEvent(data) {
|
|
98
|
+
const { subscriptionId, event } = data.payload;
|
|
99
|
+
const handler = eventHandlers.get(subscriptionId);
|
|
100
|
+
if (!handler) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
handler(event);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error('[app-sdk] graph event handler error:', error);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function handlePendingResponse(data) {
|
|
111
|
+
const request = pending.get(data.id);
|
|
112
|
+
if (!request) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
pending.delete(data.id);
|
|
116
|
+
if (data.error) {
|
|
117
|
+
request.reject(new Error(data.error));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
request.resolve(data.payload);
|
|
121
|
+
}
|
|
122
|
+
function receive(data) {
|
|
123
|
+
if (!data || typeof data.type !== 'string')
|
|
124
|
+
return;
|
|
125
|
+
if (!data.id)
|
|
126
|
+
return;
|
|
127
|
+
log('received', data.type, data.id, data.error ? `error: ${data.error}` : '');
|
|
128
|
+
switch (data.type) {
|
|
129
|
+
case SDK_MSG.COMMAND_INVOKE:
|
|
130
|
+
handleCommandInvoke(data);
|
|
131
|
+
return;
|
|
132
|
+
case SDK_MSG.OS_CONFIG_UPDATE:
|
|
133
|
+
handleOsConfigUpdate(data);
|
|
134
|
+
return;
|
|
135
|
+
case SDK_MSG.GRAPH_EVENT:
|
|
136
|
+
handleGraphEvent(data);
|
|
137
|
+
return;
|
|
138
|
+
case SDK_MSG.INIT_ACK:
|
|
139
|
+
case SDK_MSG.RESPONSE:
|
|
140
|
+
handlePendingResponse(data);
|
|
141
|
+
return;
|
|
142
|
+
default:
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const unsubscribe = channel.subscribe(receive);
|
|
147
|
+
function postToHost(message) {
|
|
148
|
+
channel.post(message);
|
|
149
|
+
}
|
|
150
|
+
function respondToHost(id, payload, error) {
|
|
151
|
+
postToHost({
|
|
152
|
+
type: SDK_MSG.RESPONSE,
|
|
153
|
+
id,
|
|
154
|
+
payload: error ? undefined : payload,
|
|
155
|
+
error,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function send(type, payload) {
|
|
159
|
+
if (_destroyed)
|
|
160
|
+
throw new Error('Bridge is destroyed — cannot send messages');
|
|
161
|
+
const id = nextId();
|
|
162
|
+
const msg = { type, id, payload };
|
|
163
|
+
log('sending', type, id);
|
|
164
|
+
postToHost(msg);
|
|
165
|
+
return id;
|
|
166
|
+
}
|
|
167
|
+
function request(type, payload, timeout = 60000) {
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const id = send(type, payload);
|
|
170
|
+
pending.set(id, { resolve, reject });
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
if (pending.has(id)) {
|
|
173
|
+
pending.delete(id);
|
|
174
|
+
reject(new Error(`SDK request ${type} timed out after ${timeout}ms`));
|
|
175
|
+
}
|
|
176
|
+
}, timeout);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const storage = {
|
|
180
|
+
get: key => request(SDK_MSG.STORAGE_GET, { key }),
|
|
181
|
+
set: (key, value) => request(SDK_MSG.STORAGE_SET, { key, value }),
|
|
182
|
+
delete: key => request(SDK_MSG.STORAGE_DELETE, { key }),
|
|
183
|
+
keys: () => request(SDK_MSG.STORAGE_KEYS, {}),
|
|
184
|
+
};
|
|
185
|
+
const baseRequest = (payload) => {
|
|
186
|
+
log('[SDK] transport.request', payload);
|
|
187
|
+
return request(SDK_MSG.TRANSPORT, payload);
|
|
188
|
+
};
|
|
189
|
+
const transport = {
|
|
190
|
+
graph: new GraphTransport(baseRequest, { debug }),
|
|
191
|
+
os: new OsTransport(baseRequest),
|
|
192
|
+
backend: routeKey => new BackendTransport(baseRequest, routeKey),
|
|
193
|
+
};
|
|
194
|
+
function idb(storeName) {
|
|
195
|
+
const normalizedStoreName = assertStoreName(storeName);
|
|
196
|
+
return {
|
|
197
|
+
get: key => request(SDK_MSG.IDB_GET, { storeName: normalizedStoreName, key }),
|
|
198
|
+
put: (key, value) => request(SDK_MSG.IDB_PUT, { storeName: normalizedStoreName, key, value }),
|
|
199
|
+
delete: key => request(SDK_MSG.IDB_DELETE, { storeName: normalizedStoreName, key }),
|
|
200
|
+
query: (options = {}) => request(SDK_MSG.IDB_QUERY, {
|
|
201
|
+
storeName: normalizedStoreName,
|
|
202
|
+
prefix: options.prefix,
|
|
203
|
+
limit: options.limit,
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
const events = {
|
|
208
|
+
subscribe({ types }, handler) {
|
|
209
|
+
const subscriptionId = nextId();
|
|
210
|
+
eventHandlers.set(subscriptionId, handler);
|
|
211
|
+
// Fire-and-forget the ack: the app-generated id removes any race, so a
|
|
212
|
+
// failed ack only means no events will arrive — surface it and clean up.
|
|
213
|
+
request(SDK_MSG.EVENT_SUBSCRIBE, { subscriptionId, types }).catch((error) => {
|
|
214
|
+
eventHandlers.delete(subscriptionId);
|
|
215
|
+
console.error('[app-sdk] events.subscribe failed:', error);
|
|
216
|
+
});
|
|
217
|
+
return () => {
|
|
218
|
+
if (!eventHandlers.delete(subscriptionId)) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (_destroyed) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
send(SDK_MSG.EVENT_UNSUBSCRIBE, { subscriptionId });
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
const bridge = {
|
|
229
|
+
get connected() {
|
|
230
|
+
return _connected;
|
|
231
|
+
},
|
|
232
|
+
get context() {
|
|
233
|
+
return _context;
|
|
234
|
+
},
|
|
235
|
+
async connect() {
|
|
236
|
+
if (_connected && _context)
|
|
237
|
+
return _context;
|
|
238
|
+
const result = await request(SDK_MSG.INIT, { appId: hintAppId }, connectTimeout);
|
|
239
|
+
_context = result;
|
|
240
|
+
_connected = true;
|
|
241
|
+
return result;
|
|
242
|
+
},
|
|
243
|
+
notify(message, level = 'info') {
|
|
244
|
+
send(SDK_MSG.NOTIFY, { message, level });
|
|
245
|
+
},
|
|
246
|
+
storage,
|
|
247
|
+
transport,
|
|
248
|
+
idb,
|
|
249
|
+
events,
|
|
250
|
+
setCommands(commands) {
|
|
251
|
+
const payload = { commands };
|
|
252
|
+
return request(SDK_MSG.SET_COMMANDS, payload);
|
|
253
|
+
},
|
|
254
|
+
setMenu(menu) {
|
|
255
|
+
const payload = { menu };
|
|
256
|
+
return request(SDK_MSG.SET_MENU, payload);
|
|
257
|
+
},
|
|
258
|
+
setActiveTab(activeTab) {
|
|
259
|
+
const payload = { activeTab };
|
|
260
|
+
return request(SDK_MSG.SET_ACTIVE_TAB, payload);
|
|
261
|
+
},
|
|
262
|
+
sendShortcutEvent(payload) {
|
|
263
|
+
send(SDK_MSG.SHORTCUT_EVENT, payload);
|
|
264
|
+
},
|
|
265
|
+
registerCommandHandlers(handlers) {
|
|
266
|
+
for (const [commandId, handler] of Object.entries(handlers)) {
|
|
267
|
+
commandHandlers.set(commandId, handler);
|
|
268
|
+
}
|
|
269
|
+
return () => {
|
|
270
|
+
for (const [commandId, handler] of Object.entries(handlers)) {
|
|
271
|
+
if (commandHandlers.get(commandId) === handler) {
|
|
272
|
+
commandHandlers.delete(commandId);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
},
|
|
277
|
+
onOsConfigUpdate(callback) {
|
|
278
|
+
osConfigUpdateCallbacks.add(callback);
|
|
279
|
+
return () => {
|
|
280
|
+
osConfigUpdateCallbacks.delete(callback);
|
|
281
|
+
};
|
|
282
|
+
},
|
|
283
|
+
/**
|
|
284
|
+
* @deprecated Legacy/internal only. Hosted apps should use scoped SDK APIs
|
|
285
|
+
* such as `idb`, `transport`, and `launchApp` instead of kernel-proxy.
|
|
286
|
+
*/
|
|
287
|
+
sendToKernel(kernelType, kernelPayload) {
|
|
288
|
+
return request(SDK_MSG.KERNEL_PROXY, { kernelType, kernelPayload });
|
|
289
|
+
},
|
|
290
|
+
launchApp(appId) {
|
|
291
|
+
return request(SDK_MSG.LAUNCH_APP, { appId });
|
|
292
|
+
},
|
|
293
|
+
destroy() {
|
|
294
|
+
if (_destroyed)
|
|
295
|
+
return;
|
|
296
|
+
log('bridge.destroy(), pending requests:', pending.size);
|
|
297
|
+
// Best-effort unsubscribe every open stream before flipping the flag.
|
|
298
|
+
for (const subscriptionId of eventHandlers.keys()) {
|
|
299
|
+
try {
|
|
300
|
+
send(SDK_MSG.EVENT_UNSUBSCRIBE, { subscriptionId });
|
|
301
|
+
}
|
|
302
|
+
catch {
|
|
303
|
+
// ignore — host will drop the frame on window close anyway
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
eventHandlers.clear();
|
|
307
|
+
_destroyed = true;
|
|
308
|
+
unsubscribe();
|
|
309
|
+
for (const [, p] of pending)
|
|
310
|
+
p.reject(new Error('Bridge destroyed'));
|
|
311
|
+
pending.clear();
|
|
312
|
+
commandHandlers.clear();
|
|
313
|
+
_connected = false;
|
|
314
|
+
_context = null;
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
return bridge;
|
|
318
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { connect } from './client.js';
|
|
|
5
5
|
export type { Bardioc } from './client.js';
|
|
6
6
|
export { BardiocError } from './errors.js';
|
|
7
7
|
export type { ErrorCode } from './errors.js';
|
|
8
|
-
export type { AppRecord, AppUpload, BffRequest, Command, Context, GraphEvent, IdbEntry, InstalledApp, MenuItem, MenuRoot, NotifyLevel, OsConfig, Shortcut, UploadResult, } from './contract.js';
|
|
8
|
+
export type { AppRecord, AppUpload, BffRequest, Command, Context, DevAppConfig, GraphEvent, IdbEntry, InstalledApp, MenuItem, MenuRoot, NotifyLevel, OsConfig, Setting, SettingValue, Shortcut, UploadResult, } from './contract.js';
|
|
9
9
|
export { DEFAULT_WINDOW_SIZE, MANIFEST_FILENAME, WINDOW_SIZES, validateManifest, } from './manifest.js';
|
|
10
10
|
export type { AppManifest, AppManifestRole, AppManifestRules, AppManifestWindowConfig, CustomWindowSize, WindowSizePreset, } from './manifest.js';
|
|
11
11
|
export type { Application, BatchResult, CombinedQueryOptions, Configuration, CreateMemberInput, CreateUnitInput, DeleteOptions, FileEntry, GetNodeOptions, GraphEdgeRaw, GraphNodeRaw, GremlinOptions, HistoryEntry, HistoryOptions, InstanceList, MutationOptions, OrgMember, OrgStructure, OrgUnit, QueryOptions, TimeseriesAggregate, TimeseriesHistory, TimeseriesHistoryOptions, TimeseriesOptions, TimeseriesQueryOptions, TimeseriesValue, TransportScopeOptions, UpdateMemberInput, UpdateUnitInput, UserProfile, } from './types.js';
|
package/dist/protocol.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { APP_METHODS, GRAPH_EVENT_TYPES, HOST_METHODS, PROTOCOL_VERSION, graphEventFilter, } from './contract.js';
|
|
2
|
-
export type { AppMethods, AppRecord, AppUpload, BffRequest, CallMethod, Command, Context, GraphEvent, HostEvents, HostMethods, IdbEntry, InstalledApp, MenuItem, MenuRoot, Method, NotifyLevel, OsConfig, Params, Result, Shortcut, StreamMethod, UploadResult, } from './contract.js';
|
|
2
|
+
export type { AppMethods, AppRecord, AppUpload, BffRequest, CallMethod, Command, Context, DevAppConfig, GraphEvent, HostEvents, HostMethods, IdbEntry, InstalledApp, MenuItem, MenuRoot, Method, NotifyLevel, OsConfig, Params, Result, Setting, SettingValue, Shortcut, StreamMethod, UploadResult, } from './contract.js';
|
|
3
3
|
export { BardiocError } from './errors.js';
|
|
4
4
|
export type { ErrorCode, WireError } from './errors.js';
|
|
5
5
|
export { acceptApp, serve } from './serve.js';
|
package/dist/react.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type DependencyList, type ReactNode } from 'react';
|
|
2
2
|
import { type Bardioc } from './client.js';
|
|
3
|
-
import type { Command, GraphEvent, MenuRoot, NotifyLevel, OsConfig } from './contract.js';
|
|
3
|
+
import type { Command, GraphEvent, MenuRoot, NotifyLevel, OsConfig, SettingValue } from './contract.js';
|
|
4
4
|
export interface AppSdkProviderProps {
|
|
5
5
|
children: ReactNode;
|
|
6
6
|
/** Apply theme, language and size to this document. Default: `true`. */
|
|
@@ -21,6 +21,8 @@ export declare function useCommandMenu(commands: AppCommand[], menu: MenuRoot[]
|
|
|
21
21
|
export declare function useActiveTab(activeTab: string | null): void;
|
|
22
22
|
/** The host's current theme, language and size. */
|
|
23
23
|
export declare function useOsConfig(): OsConfig;
|
|
24
|
+
/** The app's own settings, keyed `<group>.<field>`; follows every change the host pushes. */
|
|
25
|
+
export declare function useSettings(): Record<string, SettingValue>;
|
|
24
26
|
/** Streams graph events of `types` while mounted; `handler` may change without resubscribing. */
|
|
25
27
|
export declare function useGraphEvents({ types }: {
|
|
26
28
|
types: string[];
|
package/dist/react.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { createContext, useCallback, useContext, useEffect, useRef, useState, } from 'react';
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useRef, useState, } from 'react';
|
|
3
3
|
import { connect } from './client.js';
|
|
4
4
|
const BardiocContext = createContext(null);
|
|
5
5
|
const reportFailure = (what) => (error) => console.error(`[app-sdk] ${what} failed`, error);
|
|
@@ -25,7 +25,8 @@ export function AppSdkProvider({ children, applyOsConfig: apply = true, }) {
|
|
|
25
25
|
active = false;
|
|
26
26
|
};
|
|
27
27
|
}, []);
|
|
28
|
-
|
|
28
|
+
// Same commit as children: no frame renders with the wrong theme.
|
|
29
|
+
useLayoutEffect(() => {
|
|
29
30
|
if (!apply || !bardioc)
|
|
30
31
|
return;
|
|
31
32
|
applyOsConfig(bardioc.context.osConfig);
|
|
@@ -102,6 +103,17 @@ export function useOsConfig() {
|
|
|
102
103
|
}, [bardioc]);
|
|
103
104
|
return osConfig;
|
|
104
105
|
}
|
|
106
|
+
/** The app's own settings, keyed `<group>.<field>`; follows every change the host pushes. */
|
|
107
|
+
export function useSettings() {
|
|
108
|
+
const bardioc = useSdk();
|
|
109
|
+
const [settings, setSettings] = useState(bardioc.context.settings ?? NO_SETTINGS);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
setSettings(bardioc.context.settings ?? NO_SETTINGS);
|
|
112
|
+
return bardioc.onContextChange(context => setSettings(context.settings ?? NO_SETTINGS));
|
|
113
|
+
}, [bardioc]);
|
|
114
|
+
return settings;
|
|
115
|
+
}
|
|
116
|
+
const NO_SETTINGS = {};
|
|
105
117
|
/** Streams graph events of `types` while mounted; `handler` may change without resubscribing. */
|
|
106
118
|
export function useGraphEvents({ types }, handler, deps = []) {
|
|
107
119
|
const { events } = useSdk();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BackendRequestOptions, SdkTransportPayload } from '../types.js';
|
|
2
|
+
export declare function isRouteKey(routeKey: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Client for an application's own REST backend, reached through the BFF relay at
|
|
5
|
+
* `/relay/{routeKey}`. The host stamps the calling app id and scope; the BFF checks the
|
|
6
|
+
* app against the route's whitelist and forwards the request with an app-scoped token.
|
|
7
|
+
*/
|
|
8
|
+
export declare class BackendTransport {
|
|
9
|
+
private _request;
|
|
10
|
+
private readonly routeKey;
|
|
11
|
+
constructor(_request: <T = unknown>(payload: SdkTransportPayload) => Promise<T>, routeKey: string);
|
|
12
|
+
/**
|
|
13
|
+
* Send a request to the backend. `path` is what the backend sees, after the relay
|
|
14
|
+
* stripped `/relay/{routeKey}`. Scope is required unless stated otherwise: the relay
|
|
15
|
+
* refuses an app-scoped call without one.
|
|
16
|
+
*
|
|
17
|
+
* @throws {EntityNotFoundError} on 404 errors
|
|
18
|
+
* @throws {ValidationError} on 400 errors
|
|
19
|
+
* @throws {PermissionError} on 403 errors (app not whitelisted for the route)
|
|
20
|
+
* @throws {NetworkError} on HTTP errors, including 502 when the backend is unreachable
|
|
21
|
+
* @throws {TimeoutError} on request timeout
|
|
22
|
+
*/
|
|
23
|
+
request<T = unknown>(options: BackendRequestOptions): Promise<T>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { ValidationError } from '../errors.js';
|
|
2
|
+
import { parseOsError } from './os-transport.js';
|
|
3
|
+
const RELAY_BASE = '/relay';
|
|
4
|
+
// Mirrors the BFF's route key rule: one path segment, "." and ".." excluded.
|
|
5
|
+
const ROUTE_KEY_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
6
|
+
export function isRouteKey(routeKey) {
|
|
7
|
+
return routeKey !== '.' && routeKey !== '..' && ROUTE_KEY_PATTERN.test(routeKey);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Client for an application's own REST backend, reached through the BFF relay at
|
|
11
|
+
* `/relay/{routeKey}`. The host stamps the calling app id and scope; the BFF checks the
|
|
12
|
+
* app against the route's whitelist and forwards the request with an app-scoped token.
|
|
13
|
+
*/
|
|
14
|
+
export class BackendTransport {
|
|
15
|
+
_request;
|
|
16
|
+
routeKey;
|
|
17
|
+
constructor(_request, routeKey) {
|
|
18
|
+
this._request = _request;
|
|
19
|
+
this.routeKey = routeKey;
|
|
20
|
+
if (!isRouteKey(routeKey)) {
|
|
21
|
+
throw new ValidationError(`Invalid relay route key: "${routeKey}"`, 'routeKey');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Send a request to the backend. `path` is what the backend sees, after the relay
|
|
26
|
+
* stripped `/relay/{routeKey}`. Scope is required unless stated otherwise: the relay
|
|
27
|
+
* refuses an app-scoped call without one.
|
|
28
|
+
*
|
|
29
|
+
* @throws {EntityNotFoundError} on 404 errors
|
|
30
|
+
* @throws {ValidationError} on 400 errors
|
|
31
|
+
* @throws {PermissionError} on 403 errors (app not whitelisted for the route)
|
|
32
|
+
* @throws {NetworkError} on HTTP errors, including 502 when the backend is unreachable
|
|
33
|
+
* @throws {TimeoutError} on request timeout
|
|
34
|
+
*/
|
|
35
|
+
async request(options) {
|
|
36
|
+
const path = options.path.startsWith('/') ? options.path : `/${options.path}`;
|
|
37
|
+
try {
|
|
38
|
+
return await this._request({
|
|
39
|
+
path: `${RELAY_BASE}/${this.routeKey}${path}`,
|
|
40
|
+
method: options.method || 'GET',
|
|
41
|
+
body: options.body,
|
|
42
|
+
contentType: options.contentType,
|
|
43
|
+
scopeId: options.scopeId,
|
|
44
|
+
scopePolicy: options.scopePolicy ?? 'required',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw parseOsError(error, `backend.${this.routeKey}.request`, {
|
|
49
|
+
path: options.path,
|
|
50
|
+
method: options.method,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|