@bardioc/app-sdk 0.4.10 → 0.5.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/dist/dev-proxy.js +8 -3
- package/dist/dev.d.ts +6 -0
- package/dist/dev.js +6 -0
- package/dist/host-bridge.d.ts +1 -1
- package/dist/host-bridge.js +43 -29
- package/dist/index.d.ts +1 -1
- package/dist/react.d.ts +3 -1
- package/dist/react.js +49 -18
- package/dist/transports/backend-transport.d.ts +24 -0
- package/dist/transports/backend-transport.js +54 -0
- package/dist/transports/os-transport.d.ts +1 -0
- package/dist/transports/os-transport.js +1 -1
- package/dist/types.d.ts +12 -0
- package/package.json +1 -1
package/dist/dev-proxy.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { resolveDevAuthEnv } from './dev-auth-env.js';
|
|
3
|
+
// BFF mount of the dynamic relay to published REST backends.
|
|
4
|
+
const RELAY_PATH_PREFIX = '/relay';
|
|
3
5
|
export async function readRequestBody(req) {
|
|
4
6
|
return new Promise((resolve, reject) => {
|
|
5
7
|
const chunks = [];
|
|
@@ -95,9 +97,12 @@ export function createDevAuthProxyHandler(options) {
|
|
|
95
97
|
}
|
|
96
98
|
addDevSecretHeader(req, headers, devSecret);
|
|
97
99
|
dropOriginKeepReferer(headers);
|
|
98
|
-
// Dev auth is dev-secret only
|
|
99
|
-
//
|
|
100
|
-
|
|
100
|
+
// Dev auth is dev-secret only, so X-App-Id is dropped and the BFF app-token exchange
|
|
101
|
+
// (which would also demand X-Scope-ID) is never triggered. The relay is the exception:
|
|
102
|
+
// it authorizes on the exchanged app identity and answers 403 without one.
|
|
103
|
+
if (!upstreamPath.startsWith(`${RELAY_PATH_PREFIX}/`)) {
|
|
104
|
+
headers.delete('X-App-Id');
|
|
105
|
+
}
|
|
101
106
|
// BFF rejects /bardioc/ calls without this CSRF marker header; stamp it like the host does.
|
|
102
107
|
headers.set('X-Requested-With', 'XMLHttpRequest');
|
|
103
108
|
let body;
|
package/dist/dev.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export interface DevBridgeOptions {
|
|
2
2
|
/** Base URL prepended to SDK transport paths. Defaults to `/rest`. */
|
|
3
3
|
apiBaseUrl?: string;
|
|
4
|
+
/**
|
|
5
|
+
* The app's ogit/Auth/Application id, sent as X-App-ID on scoped calls like the host
|
|
6
|
+
* does. Needed for `transport.backend(...)`: the relay authorizes on the exchanged app
|
|
7
|
+
* identity and refuses calls without it.
|
|
8
|
+
*/
|
|
9
|
+
appId?: string;
|
|
4
10
|
debug?: boolean;
|
|
5
11
|
}
|
|
6
12
|
export declare function isInsideIframe(): boolean;
|
package/dist/dev.js
CHANGED
|
@@ -12,6 +12,7 @@ export function isDevStandalone() {
|
|
|
12
12
|
}
|
|
13
13
|
export function installDevBridge(options = {}) {
|
|
14
14
|
const apiBaseUrl = getBaseUrl(options.apiBaseUrl);
|
|
15
|
+
const appId = options.appId?.trim() || undefined;
|
|
15
16
|
const debug = options.debug ?? false;
|
|
16
17
|
const origin = window.location.origin;
|
|
17
18
|
const log = (...args) => {
|
|
@@ -49,6 +50,11 @@ export function installDevBridge(options = {}) {
|
|
|
49
50
|
}
|
|
50
51
|
if (resolvedScopeId) {
|
|
51
52
|
headers.set('X-Scope-ID', resolvedScopeId);
|
|
53
|
+
// The BFF app-token exchange rejects X-App-ID without X-Scope-ID, so a scopeless
|
|
54
|
+
// call goes out as the user, exactly as the host sends it.
|
|
55
|
+
if (appId) {
|
|
56
|
+
headers.set('X-App-ID', appId);
|
|
57
|
+
}
|
|
52
58
|
}
|
|
53
59
|
const url = `${apiBaseUrl}${payload.path}`;
|
|
54
60
|
log('[transport]', payload.method ?? 'GET', url);
|
package/dist/host-bridge.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { HostBridge, HostBridgeConfig } from './types.js';
|
|
2
|
-
/** Create a bridge between an
|
|
2
|
+
/** Create a bridge between an app and its host OS, over `postMessage` unless `config.channel` says otherwise. */
|
|
3
3
|
export declare function createHostBridge(config: HostBridgeConfig): HostBridge;
|
package/dist/host-bridge.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SDK_MSG } from './protocol.js';
|
|
2
|
+
import { BackendTransport } from './transports/backend-transport.js';
|
|
2
3
|
import { GraphTransport } from './transports/graph-transport.js';
|
|
3
4
|
import { OsTransport } from './transports/os-transport.js';
|
|
4
5
|
let counter = 0;
|
|
@@ -12,29 +13,49 @@ function assertStoreName(storeName) {
|
|
|
12
13
|
}
|
|
13
14
|
return normalized;
|
|
14
15
|
}
|
|
15
|
-
/** Create a bridge between an
|
|
16
|
+
/** Create a bridge between an app and its host OS, over `postMessage` unless `config.channel` says otherwise. */
|
|
16
17
|
export function createHostBridge(config) {
|
|
17
18
|
const { appId: hintAppId, connectTimeout = 5000, debug = false } = config;
|
|
18
|
-
const hostWindow = globalThis.window;
|
|
19
|
-
// When inside an iframe, document.referrer gives us the parent's origin.
|
|
20
|
-
const inferredHostOrigin = (() => {
|
|
21
|
-
try {
|
|
22
|
-
if (hostWindow.parent !== hostWindow && document.referrer) {
|
|
23
|
-
return new URL(document.referrer).origin;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
// ignore parse errors
|
|
28
|
-
}
|
|
29
|
-
return hostWindow.location.origin;
|
|
30
|
-
})();
|
|
31
|
-
const targetOrigin = config.targetOrigin ?? inferredHostOrigin;
|
|
32
|
-
const isInsideIframe = hostWindow.parent !== hostWindow;
|
|
33
19
|
function log(...args) {
|
|
34
20
|
if (debug)
|
|
35
21
|
console.log('[app-sdk]', ...args);
|
|
36
22
|
}
|
|
37
|
-
|
|
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();
|
|
38
59
|
let _connected = false;
|
|
39
60
|
let _context = null;
|
|
40
61
|
let _destroyed = false;
|
|
@@ -42,9 +63,6 @@ export function createHostBridge(config) {
|
|
|
42
63
|
const osConfigUpdateCallbacks = new Set();
|
|
43
64
|
const eventHandlers = new Map();
|
|
44
65
|
const pending = new Map();
|
|
45
|
-
function isAllowedMessageOrigin(origin) {
|
|
46
|
-
return origin === targetOrigin || origin === hostWindow.location.origin;
|
|
47
|
-
}
|
|
48
66
|
function handleCommandInvoke(data) {
|
|
49
67
|
const { commandId } = data.payload;
|
|
50
68
|
const handler = commandHandlers.get(commandId);
|
|
@@ -101,12 +119,7 @@ export function createHostBridge(config) {
|
|
|
101
119
|
}
|
|
102
120
|
request.resolve(data.payload);
|
|
103
121
|
}
|
|
104
|
-
function
|
|
105
|
-
if (!isAllowedMessageOrigin(event.origin)) {
|
|
106
|
-
log('ignoring message from', event.origin, '(expected', targetOrigin, ')');
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
const data = event.data;
|
|
122
|
+
function receive(data) {
|
|
110
123
|
if (!data || typeof data.type !== 'string')
|
|
111
124
|
return;
|
|
112
125
|
if (!data.id)
|
|
@@ -130,9 +143,9 @@ export function createHostBridge(config) {
|
|
|
130
143
|
return;
|
|
131
144
|
}
|
|
132
145
|
}
|
|
133
|
-
|
|
146
|
+
const unsubscribe = channel.subscribe(receive);
|
|
134
147
|
function postToHost(message) {
|
|
135
|
-
|
|
148
|
+
channel.post(message);
|
|
136
149
|
}
|
|
137
150
|
function respondToHost(id, payload, error) {
|
|
138
151
|
postToHost({
|
|
@@ -176,6 +189,7 @@ export function createHostBridge(config) {
|
|
|
176
189
|
const transport = {
|
|
177
190
|
graph: new GraphTransport(baseRequest, { debug }),
|
|
178
191
|
os: new OsTransport(baseRequest),
|
|
192
|
+
backend: routeKey => new BackendTransport(baseRequest, routeKey),
|
|
179
193
|
};
|
|
180
194
|
function idb(storeName) {
|
|
181
195
|
const normalizedStoreName = assertStoreName(storeName);
|
|
@@ -291,7 +305,7 @@ export function createHostBridge(config) {
|
|
|
291
305
|
}
|
|
292
306
|
eventHandlers.clear();
|
|
293
307
|
_destroyed = true;
|
|
294
|
-
|
|
308
|
+
unsubscribe();
|
|
295
309
|
for (const [, p] of pending)
|
|
296
310
|
p.reject(new Error('Bridge destroyed'));
|
|
297
311
|
pending.clear();
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export { SDK_CONTRACT_MATRIX, getRequiredPermissionForSdkMessage } from './contr
|
|
|
5
5
|
export { ALLOWED_KERNEL_MESSAGES, SDK_MSG } from './protocol.js';
|
|
6
6
|
export type { SdkMessageType } from './protocol.js';
|
|
7
7
|
export { EntityNotFoundError, NetworkError, PermissionError, SdkError, TimeoutError, ValidationError, } from './errors.js';
|
|
8
|
-
export type { Application, BatchResult, CombinedQueryOptions, Configuration, CreateMemberInput, CreateUnitInput, DeleteOptions, EventSubscribeRequest, EventSubscribeResponse, EventUnsubscribeRequest, EventsApi, FileEntry, GetNodeOptions, GraphEdgeRaw, GraphEvent, GraphEventPushPayload, GraphEventType, GraphNodeRaw, GraphTransport, GremlinOptions, HistoryEntry, HistoryOptions, HostBridge, HostBridgeConfig, InstanceList, MutationOptions, NotifyLevel, OrgMember, OrgStructure, OrgUnit, OsConfig, OsRequestOptions, OsTransport, QueryOptions, SdkCommandDefinition, SdkCommandHandler, SdkCommandInvokePayload, SdkIdbDeletePayload, SdkIdbEntry, SdkIdbGetPayload, SdkIdbPutPayload, SdkIdbQueryPayload, SdkIdbStore, SdkInitResponse, SdkKernelProxyPayload, SdkMenuItem, SdkMenuRoot, SdkMessage, SdkNotifyPayload, SdkResponse, SdkSetActiveTabPayload, SdkSetCommandsPayload, SdkSetMenuPayload, SdkShortcutEventPayload, SdkStorage, SdkStorageDeletePayload, SdkStorageGetPayload, SdkStorageSetPayload, SdkTransport, SdkTransportPayload, TimeseriesAggregate, TimeseriesHistory, TimeseriesHistoryOptions, TimeseriesOptions, TimeseriesQueryOptions, TimeseriesValue, TransportScopeOptions, UpdateMemberInput, UpdateUnitInput, UserProfile, } from './types.js';
|
|
8
|
+
export type { Application, BackendRequestOptions, BackendTransport, BatchResult, CombinedQueryOptions, Configuration, CreateMemberInput, CreateUnitInput, DeleteOptions, EventSubscribeRequest, EventSubscribeResponse, EventUnsubscribeRequest, EventsApi, FileEntry, GetNodeOptions, GraphEdgeRaw, GraphEvent, GraphEventPushPayload, GraphEventType, GraphNodeRaw, GraphTransport, GremlinOptions, HistoryEntry, HistoryOptions, HostBridge, HostBridgeConfig, InstanceList, MutationOptions, NotifyLevel, OrgMember, OrgStructure, OrgUnit, OsConfig, OsRequestOptions, OsTransport, QueryOptions, SdkChannel, SdkCommandDefinition, SdkCommandHandler, SdkCommandInvokePayload, SdkIdbDeletePayload, SdkIdbEntry, SdkIdbGetPayload, SdkIdbPutPayload, SdkIdbQueryPayload, SdkIdbStore, SdkInitResponse, SdkKernelProxyPayload, SdkMenuItem, SdkMenuRoot, SdkMessage, SdkNotifyPayload, SdkResponse, SdkSetActiveTabPayload, SdkSetCommandsPayload, SdkSetMenuPayload, SdkShortcutEventPayload, SdkStorage, SdkStorageDeletePayload, SdkStorageGetPayload, SdkStorageSetPayload, SdkTransport, SdkTransportPayload, TimeseriesAggregate, TimeseriesHistory, TimeseriesHistoryOptions, TimeseriesOptions, TimeseriesQueryOptions, TimeseriesValue, TransportScopeOptions, UpdateMemberInput, UpdateUnitInput, UserProfile, } from './types.js';
|
package/dist/react.d.ts
CHANGED
|
@@ -2,12 +2,14 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import type { GraphEvent, HostBridge, HostBridgeConfig, NotifyLevel, OsConfig, SdkCommandDefinition, SdkCommandHandler, SdkMenuRoot } from './types.js';
|
|
3
3
|
export interface AppSdkProviderProps extends HostBridgeConfig {
|
|
4
4
|
children: ReactNode;
|
|
5
|
+
/** Apply theme, language and size to this document. Default: `true`; `false` when the app shares the host document. */
|
|
6
|
+
applyOsConfig?: boolean;
|
|
5
7
|
}
|
|
6
8
|
export interface AppCommand extends SdkCommandDefinition {
|
|
7
9
|
handler: SdkCommandHandler;
|
|
8
10
|
}
|
|
9
11
|
/** Mounts the SDK bridge and makes it available to descendant hooks. */
|
|
10
|
-
export declare function AppSdkProvider({ children, ...config }: Readonly<AppSdkProviderProps>): import("react/jsx-runtime").JSX.Element | null;
|
|
12
|
+
export declare function AppSdkProvider({ children, applyOsConfig, ...config }: Readonly<AppSdkProviderProps>): import("react/jsx-runtime").JSX.Element | null;
|
|
11
13
|
/** Returns the raw {@link HostBridge}. Must be used inside `<AppSdkProvider>`. */
|
|
12
14
|
export declare function useSdk(): HostBridge;
|
|
13
15
|
/** Returns a stable callback that fires a host notification toast. */
|
package/dist/react.js
CHANGED
|
@@ -2,6 +2,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useRef, useState, } from 'react';
|
|
3
3
|
import { createHostBridge } from './host-bridge.js';
|
|
4
4
|
const SdkContext = createContext(null);
|
|
5
|
+
// One handshake per bridge, however often the effect that owns it reconnects.
|
|
6
|
+
const connections = new WeakMap();
|
|
5
7
|
const SYSTEM_SIZE_CLASS_NAMES = ['text-sm', 'text-base', 'text-lg', 'text-xl'];
|
|
6
8
|
function applyOsConfigToDocument(osConfig) {
|
|
7
9
|
if (!osConfig) {
|
|
@@ -39,28 +41,46 @@ function hasShortcutRoots(menu) {
|
|
|
39
41
|
return menu.some(root => hasShortcutItems(root.items));
|
|
40
42
|
}
|
|
41
43
|
/** Mounts the SDK bridge and makes it available to descendant hooks. */
|
|
42
|
-
export function AppSdkProvider({ children, ...config }) {
|
|
44
|
+
export function AppSdkProvider({ children, applyOsConfig = true, ...config }) {
|
|
43
45
|
const bridgeRef = useRef(null);
|
|
44
|
-
const { appId, connectTimeout, debug, targetOrigin } = config;
|
|
46
|
+
const { appId, channel, connectTimeout, debug, targetOrigin } = config;
|
|
45
47
|
const configKey = JSON.stringify([
|
|
46
48
|
appId ?? null,
|
|
47
49
|
connectTimeout ?? null,
|
|
48
50
|
debug ?? false,
|
|
49
51
|
targetOrigin ?? null,
|
|
50
52
|
]);
|
|
51
|
-
const bridgeConfig = { appId, connectTimeout, debug, targetOrigin };
|
|
53
|
+
const bridgeConfig = { appId, channel, connectTimeout, debug, targetOrigin };
|
|
52
54
|
const bridgeConfigKeyRef = useRef(null);
|
|
55
|
+
const pendingDestroyRef = useRef(null);
|
|
53
56
|
const [readyConfigKey, setReadyConfigKey] = useState(null);
|
|
54
57
|
const [errorState, setErrorState] = useState(null);
|
|
55
58
|
useEffect(() => {
|
|
56
59
|
let cancelled = false;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
+
// A disconnect followed at once by a reconnect (StrictMode, Activity) keeps the live bridge:
|
|
61
|
+
// the children holding it reconnect their own effects over it before this one runs.
|
|
62
|
+
const pending = pendingDestroyRef.current;
|
|
63
|
+
pendingDestroyRef.current = null;
|
|
64
|
+
if (pending)
|
|
65
|
+
clearTimeout(pending.timer);
|
|
66
|
+
let bridge;
|
|
67
|
+
if (pending && pending.configKey === configKey && pending.channel === channel) {
|
|
68
|
+
bridge = pending.bridge;
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
pending?.destroy();
|
|
72
|
+
setReadyConfigKey(null);
|
|
73
|
+
setErrorState(null);
|
|
74
|
+
bridge = createHostBridge(bridgeConfig);
|
|
75
|
+
}
|
|
60
76
|
bridgeRef.current = bridge;
|
|
61
77
|
bridgeConfigKeyRef.current = configKey;
|
|
62
|
-
bridge
|
|
63
|
-
|
|
78
|
+
let connecting = connections.get(bridge);
|
|
79
|
+
if (!connecting) {
|
|
80
|
+
connecting = bridge.connect();
|
|
81
|
+
connections.set(bridge, connecting);
|
|
82
|
+
}
|
|
83
|
+
connecting
|
|
64
84
|
.then(() => {
|
|
65
85
|
if (!cancelled) {
|
|
66
86
|
setReadyConfigKey(configKey);
|
|
@@ -75,17 +95,28 @@ export function AppSdkProvider({ children, ...config }) {
|
|
|
75
95
|
});
|
|
76
96
|
return () => {
|
|
77
97
|
cancelled = true;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
bridgeRef.current
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
98
|
+
const destroy = () => {
|
|
99
|
+
bridge.destroy();
|
|
100
|
+
if (bridgeRef.current === bridge) {
|
|
101
|
+
bridgeRef.current = null;
|
|
102
|
+
bridgeConfigKeyRef.current = null;
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
pendingDestroyRef.current = {
|
|
106
|
+
bridge,
|
|
107
|
+
configKey,
|
|
108
|
+
channel,
|
|
109
|
+
destroy,
|
|
110
|
+
timer: setTimeout(() => {
|
|
111
|
+
pendingDestroyRef.current = null;
|
|
112
|
+
destroy();
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
85
115
|
};
|
|
86
|
-
}, [appId, configKey, connectTimeout, debug, targetOrigin]);
|
|
116
|
+
}, [appId, channel, configKey, connectTimeout, debug, targetOrigin]);
|
|
87
117
|
useEffect(() => {
|
|
88
|
-
if (
|
|
118
|
+
if (!applyOsConfig ||
|
|
119
|
+
readyConfigKey !== configKey ||
|
|
89
120
|
bridgeConfigKeyRef.current !== configKey ||
|
|
90
121
|
!bridgeRef.current) {
|
|
91
122
|
return;
|
|
@@ -95,7 +126,7 @@ export function AppSdkProvider({ children, ...config }) {
|
|
|
95
126
|
applyOsConfigToDocument(newConfig);
|
|
96
127
|
});
|
|
97
128
|
return unsubscribe;
|
|
98
|
-
}, [configKey, readyConfigKey]);
|
|
129
|
+
}, [applyOsConfig, configKey, readyConfigKey]);
|
|
99
130
|
if (errorState?.configKey === configKey) {
|
|
100
131
|
return _jsx("div", { "data-sdk-error": true, children: errorState.message });
|
|
101
132
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Application, Configuration, CreateMemberInput, CreateUnitInput, FileEntry, InstanceList, OrgMember, OrgStructure, OrgUnit, OsRequestOptions, SdkTransportPayload, UpdateMemberInput, UpdateUnitInput, UserProfile } from '../types.js';
|
|
2
|
+
export declare function parseOsError(error: any, operation: string, context?: Record<string, unknown>): Error;
|
|
2
3
|
/** OS transport class providing OS/system operations */
|
|
3
4
|
export declare class OsTransport {
|
|
4
5
|
private readonly _request;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EntityNotFoundError, NetworkError, PermissionError, SdkError, TimeoutError, ValidationError, } from '../errors.js';
|
|
2
2
|
const OS_BASE = '/os';
|
|
3
3
|
// Maps backend errors to typed SDK errors
|
|
4
|
-
function parseOsError(error, operation, context) {
|
|
4
|
+
export function parseOsError(error, operation, context) {
|
|
5
5
|
let statusCode;
|
|
6
6
|
let errorMessage = error.message || 'Unknown error';
|
|
7
7
|
if (error.responsePayload && typeof error.responsePayload === 'object') {
|
package/dist/types.d.ts
CHANGED
|
@@ -168,9 +168,18 @@ export interface SdkIdbQueryPayload {
|
|
|
168
168
|
prefix?: string;
|
|
169
169
|
limit?: number;
|
|
170
170
|
}
|
|
171
|
+
/** How bridge messages travel between an app and its host. */
|
|
172
|
+
export interface SdkChannel {
|
|
173
|
+
/** Delivers one message to the host. */
|
|
174
|
+
post(message: SdkMessage | SdkResponse): void;
|
|
175
|
+
/** Hands every message from the host to `receive`; returns the unsubscribe. */
|
|
176
|
+
subscribe(receive: (message: SdkResponse) => void): () => void;
|
|
177
|
+
}
|
|
171
178
|
/** Configuration passed to {@link createHostBridge}. */
|
|
172
179
|
export interface HostBridgeConfig {
|
|
173
180
|
appId?: string;
|
|
181
|
+
/** Message transport. Default: `postMessage` with the parent window. */
|
|
182
|
+
channel?: SdkChannel;
|
|
174
183
|
/** Override the host origin for `postMessage`. Inferred from `document.referrer` by default. */
|
|
175
184
|
targetOrigin?: string;
|
|
176
185
|
/** Handshake timeout in ms. Default: `5000`. */
|
|
@@ -202,6 +211,7 @@ export interface SdkIdbStore {
|
|
|
202
211
|
export interface SdkTransport {
|
|
203
212
|
graph: GraphTransport;
|
|
204
213
|
os: OsTransport;
|
|
214
|
+
backend(routeKey: string): BackendTransport;
|
|
205
215
|
}
|
|
206
216
|
/** Graph node in raw backend format (ogit/* fields) */
|
|
207
217
|
export interface GraphNodeRaw {
|
|
@@ -363,6 +373,7 @@ export interface OsRequestOptions extends TransportScopeOptions {
|
|
|
363
373
|
contentType?: string;
|
|
364
374
|
scopePolicy?: 'none' | 'optional' | 'required';
|
|
365
375
|
}
|
|
376
|
+
export type BackendRequestOptions = OsRequestOptions;
|
|
366
377
|
/** Live connection between an iframe app and its host OS. */
|
|
367
378
|
export interface HostBridge {
|
|
368
379
|
/** Perform the SDK handshake. Resolves with host context. Cached after first call. */
|
|
@@ -392,3 +403,4 @@ export interface HostBridge {
|
|
|
392
403
|
/** Forward declarations for transport classes */
|
|
393
404
|
export type GraphTransport = import('./transports/graph-transport.js').GraphTransport;
|
|
394
405
|
export type OsTransport = import('./transports/os-transport.js').OsTransport;
|
|
406
|
+
export type BackendTransport = import('./transports/backend-transport.js').BackendTransport;
|