@bardioc/app-sdk 0.4.11 → 1.0.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/README.md +86 -253
- package/dist/api/graph.d.ts +30 -0
- package/dist/api/graph.js +139 -0
- package/dist/api/os.d.ts +30 -0
- package/dist/api/os.js +48 -0
- package/dist/api/relay.d.ts +7 -0
- package/dist/api/relay.js +16 -0
- package/dist/api/request.d.ts +18 -0
- package/dist/api/request.js +12 -0
- package/dist/client.d.ts +104 -0
- package/dist/client.js +106 -0
- package/dist/contract.d.ts +294 -0
- package/dist/contract.js +150 -0
- package/dist/dev-idb.d.ts +10 -0
- package/dist/dev-idb.js +42 -0
- package/dist/dev-proxy.js +17 -3
- package/dist/dev.d.ts +16 -12
- package/dist/dev.js +101 -119
- package/dist/errors.d.ts +24 -25
- package/dist/errors.js +33 -48
- package/dist/index.d.ts +10 -7
- package/dist/index.js +2 -4
- package/dist/manifest.d.ts +10 -2
- package/dist/manifest.js +28 -24
- package/dist/protocol.d.ts +6 -29
- package/dist/protocol.js +3 -31
- package/dist/react.d.ts +22 -26
- package/dist/react.js +88 -181
- package/dist/serve.d.ts +26 -0
- package/dist/serve.js +64 -0
- package/dist/testing.d.ts +17 -0
- package/dist/testing.js +128 -0
- package/dist/types.d.ts +0 -241
- package/dist/vite.js +3 -0
- package/dist/wire.d.ts +46 -0
- package/dist/wire.js +138 -0
- package/package.json +12 -4
- package/dist/contract-matrix.d.ts +0 -148
- package/dist/contract-matrix.js +0 -150
- package/dist/host-bridge.d.ts +0 -3
- package/dist/host-bridge.js +0 -306
- package/dist/transports/backend-transport.d.ts +0 -24
- package/dist/transports/backend-transport.js +0 -54
- package/dist/transports/graph-transport.d.ts +0 -241
- package/dist/transports/graph-transport.js +0 -616
- package/dist/transports/os-transport.d.ts +0 -190
- package/dist/transports/os-transport.js +0 -444
package/dist/contract.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import * as z from 'zod/mini';
|
|
2
|
+
export const PROTOCOL_VERSION = 1;
|
|
3
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
4
|
+
const MAX_MENU_DEPTH = 3;
|
|
5
|
+
const call = (input) => ({
|
|
6
|
+
returns: (timeoutMs = DEFAULT_TIMEOUT_MS) => ({
|
|
7
|
+
kind: 'call',
|
|
8
|
+
input,
|
|
9
|
+
timeoutMs,
|
|
10
|
+
}),
|
|
11
|
+
});
|
|
12
|
+
const stream = (input) => ({
|
|
13
|
+
emits: () => ({ kind: 'stream', input, timeoutMs: DEFAULT_TIMEOUT_MS }),
|
|
14
|
+
});
|
|
15
|
+
const text = z.string().check(z.minLength(1));
|
|
16
|
+
const optionalText = z.optional(z.string());
|
|
17
|
+
const optionalFlag = z.optional(z.boolean());
|
|
18
|
+
const count = z.int().check(z.gte(0));
|
|
19
|
+
// Checked by brand: a transferred buffer belongs to whichever realm received it.
|
|
20
|
+
const bytes = z.custom(value => Object.prototype.toString.call(value) === '[object ArrayBuffer]', { message: 'Expected an ArrayBuffer' });
|
|
21
|
+
// Resolved like the BFF resolves it, so `/x/../auth` cannot reach the shell's session.
|
|
22
|
+
const isAppPath = (path) => path.startsWith('/') && !/^\/auth(\/|$)/i.test(new URL(path, 'http://bff').pathname);
|
|
23
|
+
const Command = z.strictObject({
|
|
24
|
+
id: text,
|
|
25
|
+
title: optionalText,
|
|
26
|
+
disabled: optionalFlag,
|
|
27
|
+
checked: optionalFlag,
|
|
28
|
+
});
|
|
29
|
+
const MenuItem = z.lazy(() => z.discriminatedUnion('type', [
|
|
30
|
+
z.strictObject({
|
|
31
|
+
type: z.literal('command'),
|
|
32
|
+
commandId: text,
|
|
33
|
+
label: optionalText,
|
|
34
|
+
leftIcon: optionalText,
|
|
35
|
+
separatorAbove: optionalFlag,
|
|
36
|
+
shortcut: optionalText,
|
|
37
|
+
variant: z.optional(z.enum(['default', 'destructive'])),
|
|
38
|
+
}),
|
|
39
|
+
z.strictObject({
|
|
40
|
+
type: z.literal('submenu'),
|
|
41
|
+
label: text,
|
|
42
|
+
leftIcon: optionalText,
|
|
43
|
+
separatorAbove: optionalFlag,
|
|
44
|
+
contentWidth: optionalText,
|
|
45
|
+
items: z.array(MenuItem),
|
|
46
|
+
}),
|
|
47
|
+
]));
|
|
48
|
+
const depthOf = (items) => Math.max(0, ...items.map(item => (item.type === 'submenu' ? 1 + depthOf(item.items) : 1)));
|
|
49
|
+
const MenuRoot = z.strictObject({
|
|
50
|
+
label: text,
|
|
51
|
+
kind: z.optional(z.enum(['app', 'menu'])),
|
|
52
|
+
leftIcon: optionalText,
|
|
53
|
+
contentWidth: optionalText,
|
|
54
|
+
items: z.array(MenuItem),
|
|
55
|
+
});
|
|
56
|
+
const Menu = z.array(MenuRoot).check(z.refine(roots => roots.every(root => depthOf(root.items) <= MAX_MENU_DEPTH), {
|
|
57
|
+
message: `menus nest at most ${MAX_MENU_DEPTH} levels deep`,
|
|
58
|
+
}));
|
|
59
|
+
const Notify = z.strictObject({
|
|
60
|
+
message: text,
|
|
61
|
+
level: z.enum(['success', 'error', 'info', 'warning']),
|
|
62
|
+
});
|
|
63
|
+
const Shortcut = z.strictObject({
|
|
64
|
+
code: text,
|
|
65
|
+
ctrlKey: z.boolean(),
|
|
66
|
+
metaKey: z.boolean(),
|
|
67
|
+
altKey: z.boolean(),
|
|
68
|
+
shiftKey: z.boolean(),
|
|
69
|
+
});
|
|
70
|
+
const BffRequest = z
|
|
71
|
+
.strictObject({
|
|
72
|
+
path: z
|
|
73
|
+
.string()
|
|
74
|
+
.check(z.refine(isAppPath, { message: 'path must start with / outside /auth' })),
|
|
75
|
+
method: z.optional(z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])),
|
|
76
|
+
body: z.optional(z.unknown()),
|
|
77
|
+
contentType: optionalText,
|
|
78
|
+
scopeId: z.optional(text),
|
|
79
|
+
scopePolicy: z.optional(z.enum(['none', 'optional', 'required'])),
|
|
80
|
+
})
|
|
81
|
+
.check(z.refine(({ scopeId, scopePolicy }) => !(scopeId && scopePolicy === 'none'), {
|
|
82
|
+
message: 'scopePolicy none takes no scopeId',
|
|
83
|
+
}));
|
|
84
|
+
const IdbKey = z.strictObject({ store: text, key: text });
|
|
85
|
+
const IdbPut = z.strictObject({ store: text, key: text, value: z.unknown() });
|
|
86
|
+
const IdbQuery = z.strictObject({
|
|
87
|
+
store: text,
|
|
88
|
+
prefix: optionalText,
|
|
89
|
+
limit: z.optional(count),
|
|
90
|
+
});
|
|
91
|
+
const GraphFilter = z.strictObject({ types: z.array(text).check(z.minLength(1)) });
|
|
92
|
+
const AppSource = z.enum(['uploaded', 'dev']);
|
|
93
|
+
const AppId = z.strictObject({ id: text });
|
|
94
|
+
const AppRecord = z.strictObject({
|
|
95
|
+
id: text,
|
|
96
|
+
name: text,
|
|
97
|
+
version: text,
|
|
98
|
+
launchUrl: text,
|
|
99
|
+
source: AppSource,
|
|
100
|
+
description: optionalText,
|
|
101
|
+
icon: optionalText,
|
|
102
|
+
appId: optionalText,
|
|
103
|
+
bundleSize: z.optional(count),
|
|
104
|
+
fileCount: z.optional(count),
|
|
105
|
+
});
|
|
106
|
+
const AppUpload = z.strictObject({
|
|
107
|
+
alias: text,
|
|
108
|
+
name: text,
|
|
109
|
+
files: z.array(z.strictObject({ path: text, content: bytes })).check(z.minLength(1)),
|
|
110
|
+
appId: optionalText,
|
|
111
|
+
source: z.optional(AppSource),
|
|
112
|
+
});
|
|
113
|
+
export const GRAPH_EVENT_TYPES = [
|
|
114
|
+
'CREATE',
|
|
115
|
+
'REPLACE',
|
|
116
|
+
'UPDATE',
|
|
117
|
+
'CONNECT',
|
|
118
|
+
'DISCONNECT',
|
|
119
|
+
'DELETE',
|
|
120
|
+
'WRITE_TIMESERIES',
|
|
121
|
+
];
|
|
122
|
+
/** Methods an app calls on its host. */
|
|
123
|
+
export const APP_METHODS = {
|
|
124
|
+
'ui.notify': call(Notify).returns(),
|
|
125
|
+
'ui.setCommands': call(z.strictObject({ commands: z.array(Command) })).returns(),
|
|
126
|
+
'ui.setMenu': call(z.strictObject({ menu: z.nullable(Menu) })).returns(),
|
|
127
|
+
'ui.setActiveTab': call(z.strictObject({ activeTab: z.nullable(z.string()) })).returns(),
|
|
128
|
+
'ui.shortcut': call(Shortcut).returns(),
|
|
129
|
+
'bff.request': call(BffRequest).returns(60_000),
|
|
130
|
+
'events.graph': stream(GraphFilter).emits(),
|
|
131
|
+
'idb.get': call(IdbKey).returns(),
|
|
132
|
+
'idb.put': call(IdbPut).returns(),
|
|
133
|
+
'idb.delete': call(IdbKey).returns(),
|
|
134
|
+
'idb.query': call(IdbQuery).returns(),
|
|
135
|
+
'apps.list': call(z.undefined()).returns(),
|
|
136
|
+
'apps.get': call(AppId).returns(),
|
|
137
|
+
'apps.save': call(AppRecord).returns(),
|
|
138
|
+
'apps.delete': call(AppId).returns(),
|
|
139
|
+
'apps.upload': call(AppUpload).returns(120_000),
|
|
140
|
+
'apps.launch': call(AppId).returns(),
|
|
141
|
+
};
|
|
142
|
+
/** Methods a host calls on its app. */
|
|
143
|
+
export const HOST_METHODS = {
|
|
144
|
+
'ui.command': call(z.strictObject({ id: text })).returns(),
|
|
145
|
+
};
|
|
146
|
+
/** The BFF event-stream filter matching any of `types`. */
|
|
147
|
+
export function graphEventFilter(types) {
|
|
148
|
+
const predicates = types.map(type => `(element.ogit/_type=${type})`);
|
|
149
|
+
return predicates.length > 1 ? `|${predicates.join('')}` : predicates.join('');
|
|
150
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AppMethods, IdbEntry, Params } from './contract.js';
|
|
2
|
+
type In<K extends keyof AppMethods> = Params<AppMethods[K]>;
|
|
3
|
+
/** The host's app storage, kept in this browser's IndexedDB while running standalone. */
|
|
4
|
+
export declare function devIdb(appId: string): {
|
|
5
|
+
get: ({ store, key }: In<"idb.get">) => Promise<IdbEntry<unknown> | null>;
|
|
6
|
+
put: ({ store, key, value }: In<"idb.put">) => Promise<void>;
|
|
7
|
+
delete: ({ store, key }: In<"idb.delete">) => Promise<void>;
|
|
8
|
+
query: ({ store, prefix, limit }: In<"idb.query">) => Promise<IdbEntry<unknown>[]>;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
package/dist/dev-idb.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const ENTRIES = 'entries';
|
|
2
|
+
// Sorts after every key that starts with a given prefix.
|
|
3
|
+
const PREFIX_END = String.fromCharCode(0xffff);
|
|
4
|
+
const settled = (request) => new Promise((resolve, reject) => {
|
|
5
|
+
request.onsuccess = () => resolve(request.result);
|
|
6
|
+
request.onerror = () => reject(request.error);
|
|
7
|
+
});
|
|
8
|
+
function open(name) {
|
|
9
|
+
const request = indexedDB.open(name, 1);
|
|
10
|
+
request.onupgradeneeded = () => request.result.createObjectStore(ENTRIES, { keyPath: ['store', 'key'] });
|
|
11
|
+
return settled(request);
|
|
12
|
+
}
|
|
13
|
+
const toEntry = ({ key, value, updatedAt }) => ({ key, value, updatedAt });
|
|
14
|
+
/** The host's app storage, kept in this browser's IndexedDB while running standalone. */
|
|
15
|
+
export function devIdb(appId) {
|
|
16
|
+
// Opened on first use: most apps never touch their store.
|
|
17
|
+
let db;
|
|
18
|
+
const entries = async (mode) => {
|
|
19
|
+
db ??= open(`bardioc-dev:${appId}`);
|
|
20
|
+
return (await db).transaction(ENTRIES, mode).objectStore(ENTRIES);
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
get: async ({ store, key }) => {
|
|
24
|
+
const row = await settled((await entries('readonly')).get([store, key]));
|
|
25
|
+
return row ? toEntry(row) : null;
|
|
26
|
+
},
|
|
27
|
+
put: async ({ store, key, value }) => {
|
|
28
|
+
const row = { store, key, value, updatedAt: Date.now() };
|
|
29
|
+
await settled((await entries('readwrite')).put(row));
|
|
30
|
+
},
|
|
31
|
+
delete: async ({ store, key }) => {
|
|
32
|
+
await settled((await entries('readwrite')).delete([store, key]));
|
|
33
|
+
},
|
|
34
|
+
query: async ({ store, prefix = '', limit }) => {
|
|
35
|
+
if (limit === 0)
|
|
36
|
+
return [];
|
|
37
|
+
const range = IDBKeyRange.bound([store, prefix], [store, prefix + PREFIX_END]);
|
|
38
|
+
const rows = await settled((await entries('readonly')).getAll(range, limit));
|
|
39
|
+
return rows.map(toEntry);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/dev-proxy.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
|
+
import { Readable } from 'node:stream';
|
|
2
3
|
import { resolveDevAuthEnv } from './dev-auth-env.js';
|
|
3
4
|
// BFF mount of the dynamic relay to published REST backends.
|
|
4
5
|
const RELAY_PATH_PREFIX = '/relay';
|
|
@@ -113,12 +114,16 @@ export function createDevAuthProxyHandler(options) {
|
|
|
113
114
|
sendJson(res, 400, { error: 'invalid_body' }, { allowOrigin, requestOrigin });
|
|
114
115
|
return;
|
|
115
116
|
}
|
|
117
|
+
// A client that goes away (a closed event stream) ends the upstream request too.
|
|
118
|
+
const upstream = new AbortController();
|
|
119
|
+
res.on('close', () => upstream.abort());
|
|
116
120
|
try {
|
|
117
121
|
const upstreamRes = await fetch(upstreamUrl, {
|
|
118
122
|
method: req.method ?? 'GET',
|
|
119
123
|
headers,
|
|
120
124
|
body: body.length > 0 ? new Uint8Array(body) : undefined,
|
|
121
125
|
cache: 'no-store',
|
|
126
|
+
signal: upstream.signal,
|
|
122
127
|
});
|
|
123
128
|
applyCors(res, allowOrigin, requestOrigin);
|
|
124
129
|
res.statusCode = upstreamRes.status;
|
|
@@ -127,11 +132,20 @@ export function createDevAuthProxyHandler(options) {
|
|
|
127
132
|
if (upstreamContentType) {
|
|
128
133
|
res.setHeader('Content-Type', upstreamContentType);
|
|
129
134
|
}
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
// Streamed, so server-sent events reach the app as they arrive.
|
|
136
|
+
if (upstreamRes.body) {
|
|
137
|
+
for await (const chunk of Readable.fromWeb(upstreamRes.body)) {
|
|
138
|
+
res.write(chunk);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
res.end();
|
|
132
142
|
}
|
|
133
143
|
catch {
|
|
134
|
-
|
|
144
|
+
// Once streaming, the only failure left is the client leaving.
|
|
145
|
+
if (res.headersSent)
|
|
146
|
+
res.end();
|
|
147
|
+
else
|
|
148
|
+
sendJson(res, 502, { error: 'upstream_unavailable' }, { allowOrigin, requestOrigin });
|
|
135
149
|
}
|
|
136
150
|
};
|
|
137
151
|
}
|
package/dist/dev.d.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
/** Base URL prepended to SDK transport paths. Defaults to `/rest`. */
|
|
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;
|
|
10
|
-
debug?: boolean;
|
|
11
|
-
}
|
|
1
|
+
import { type HostHandlers } from './serve.js';
|
|
12
2
|
export declare function isInsideIframe(): boolean;
|
|
13
3
|
export declare function isDevStandalone(): boolean;
|
|
14
|
-
export
|
|
4
|
+
export interface DevHostOptions {
|
|
5
|
+
/** The app's manifest id, handed to the app as `context.appId`. */
|
|
6
|
+
appId: string;
|
|
7
|
+
/** The app's ogit/Auth/Application id, sent as `X-App-ID` on scoped calls; defaults to `appId`. */
|
|
8
|
+
clientId?: string;
|
|
9
|
+
/** Where the dev-auth proxy answers `/rest`. Default: `/rest` on this origin. */
|
|
10
|
+
apiBaseUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
/** The dev host's answers to every app method. */
|
|
13
|
+
export declare function devHandlers({ appId, clientId, apiBaseUrl, }: DevHostOptions): HostHandlers;
|
|
14
|
+
/**
|
|
15
|
+
* Stands in for the host while the app runs standalone in development: `connect()` reaches it
|
|
16
|
+
* the way it reaches the host, and BFF calls go through the dev-auth proxy.
|
|
17
|
+
*/
|
|
18
|
+
export declare function installDevHost(options: DevHostOptions): () => void;
|
package/dist/dev.js
CHANGED
|
@@ -1,132 +1,114 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
1
|
+
import { GRAPH_EVENT_TYPES, graphEventFilter, } from './contract.js';
|
|
2
|
+
import { devIdb } from './dev-idb.js';
|
|
3
|
+
import { BardiocError } from './errors.js';
|
|
4
|
+
import { acceptApp, serve } from './serve.js';
|
|
5
5
|
export function isInsideIframe() {
|
|
6
|
-
|
|
7
|
-
return currentWindow.parent !== currentWindow;
|
|
6
|
+
return window.parent !== window;
|
|
8
7
|
}
|
|
9
8
|
export function isDevStandalone() {
|
|
10
|
-
const
|
|
11
|
-
return Boolean(
|
|
9
|
+
const env = import.meta.env;
|
|
10
|
+
return Boolean(env?.DEV) && !isInsideIframe();
|
|
12
11
|
}
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const log = (...args) => {
|
|
19
|
-
if (debug) {
|
|
20
|
-
console.log('[bardioc-dev-bridge]', ...args);
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
function reply(id, payload) {
|
|
24
|
-
window.postMessage({ type: SDK_MSG.RESPONSE, id, payload }, origin);
|
|
12
|
+
async function readBody(response) {
|
|
13
|
+
const type = response.headers.get('content-type') ?? '';
|
|
14
|
+
if (type.includes('application/json')) {
|
|
15
|
+
const text = await response.text();
|
|
16
|
+
return text ? JSON.parse(text) : null;
|
|
25
17
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (body && payload.contentType) {
|
|
40
|
-
headers.set('Content-Type', payload.contentType);
|
|
41
|
-
}
|
|
42
|
-
// BFF rejects /bardioc/ calls without this CSRF marker header.
|
|
43
|
-
headers.set('X-Requested-With', 'XMLHttpRequest');
|
|
44
|
-
// Mirror the host bridge: a required scope without an id is rejected locally, and a
|
|
45
|
-
// resolved scope id is always forwarded regardless of policy. Standalone has no
|
|
46
|
-
// instance, so there is no instance-based fallback here.
|
|
47
|
-
const resolvedScopeId = payload.scopeId;
|
|
48
|
-
if (payload.scopePolicy === 'required' && !resolvedScopeId) {
|
|
49
|
-
throw new Error('Scope ID is required');
|
|
18
|
+
if (type === '' || type.startsWith('text/'))
|
|
19
|
+
return (await response.text()) || null;
|
|
20
|
+
return response.blob();
|
|
21
|
+
}
|
|
22
|
+
const unavailable = () => {
|
|
23
|
+
throw new BardiocError('unavailable', 'Installed apps live in the Bardioc host');
|
|
24
|
+
};
|
|
25
|
+
/** The dev host's answers to every app method. */
|
|
26
|
+
export function devHandlers({ appId, clientId = appId, apiBaseUrl = '/rest', }) {
|
|
27
|
+
const idb = devIdb(appId);
|
|
28
|
+
const request = async ({ path, method = 'GET', body, contentType, scopeId, scopePolicy }, signal) => {
|
|
29
|
+
if (scopePolicy === 'required' && !scopeId) {
|
|
30
|
+
throw new BardiocError('invalid', 'scopeId is required: a dev session has no instance');
|
|
50
31
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
32
|
+
const headers = new Headers({ 'X-Requested-With': 'XMLHttpRequest' });
|
|
33
|
+
if (contentType && body !== undefined)
|
|
34
|
+
headers.set('Content-Type', contentType);
|
|
35
|
+
if (scopeId) {
|
|
36
|
+
headers.set('X-Scope-ID', scopeId);
|
|
37
|
+
headers.set('X-App-ID', clientId);
|
|
58
38
|
}
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
method: payload.method ?? 'GET',
|
|
39
|
+
const encoded = contentType === 'application/json' ? JSON.stringify(body) : body;
|
|
40
|
+
const response = await fetch(`${apiBaseUrl}${path}`, {
|
|
41
|
+
method,
|
|
63
42
|
headers,
|
|
64
|
-
body,
|
|
43
|
+
body: body === undefined ? undefined : encoded,
|
|
65
44
|
credentials: 'include',
|
|
66
45
|
cache: 'no-store',
|
|
46
|
+
signal,
|
|
67
47
|
});
|
|
68
|
-
const
|
|
69
|
-
if (!response.ok)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (contentType.includes('application/json')) {
|
|
79
|
-
const text = await response.text();
|
|
80
|
-
return text ? JSON.parse(text) : null;
|
|
81
|
-
}
|
|
82
|
-
if (contentType === '' || contentType.startsWith('text/')) {
|
|
83
|
-
const text = await response.text();
|
|
84
|
-
return text || null;
|
|
85
|
-
}
|
|
86
|
-
return response.blob();
|
|
87
|
-
}
|
|
88
|
-
async function handleKernelProxy(payload) {
|
|
89
|
-
if (payload.kernelType === 'PING') {
|
|
90
|
-
return { pong: true };
|
|
91
|
-
}
|
|
92
|
-
if (payload.kernelType === 'GET_SESSION_STATE') {
|
|
93
|
-
return { authenticated: true };
|
|
94
|
-
}
|
|
95
|
-
throw new Error(`Kernel message ${payload.kernelType} not supported in dev bridge`);
|
|
96
|
-
}
|
|
97
|
-
async function onMessage(event) {
|
|
98
|
-
if (event.origin !== origin) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
const data = event.data;
|
|
102
|
-
if (!data || typeof data.type !== 'string' || !data.id) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (data.type === SDK_MSG.RESPONSE || data.type === SDK_MSG.INIT_ACK) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
log('handle', data.type, data.id);
|
|
109
|
-
try {
|
|
110
|
-
switch (data.type) {
|
|
111
|
-
case SDK_MSG.INIT:
|
|
112
|
-
ack(data.id, { appId: null, instanceId: null });
|
|
113
|
-
return;
|
|
114
|
-
case SDK_MSG.NOTIFY:
|
|
115
|
-
return;
|
|
116
|
-
case SDK_MSG.TRANSPORT:
|
|
117
|
-
reply(data.id, await handleTransport(data.payload));
|
|
118
|
-
return;
|
|
119
|
-
case SDK_MSG.KERNEL_PROXY:
|
|
120
|
-
reply(data.id, await handleKernelProxy(data.payload));
|
|
121
|
-
return;
|
|
122
|
-
default:
|
|
123
|
-
replyError(data.id, `Dev bridge does not implement ${data.type}`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
replyError(data.id, error instanceof Error ? error.message : 'unknown error');
|
|
48
|
+
const payload = await readBody(response);
|
|
49
|
+
if (!response.ok)
|
|
50
|
+
throw BardiocError.fromStatus(response.status, `HTTP ${response.status}`, payload);
|
|
51
|
+
return payload;
|
|
52
|
+
};
|
|
53
|
+
const graphEvents = ({ types }, emit) => {
|
|
54
|
+
const query = new URLSearchParams({ filter: graphEventFilter(types) });
|
|
55
|
+
const source = new EventSource(`${apiBaseUrl}/events?${query}`, { withCredentials: true });
|
|
56
|
+
for (const type of GRAPH_EVENT_TYPES) {
|
|
57
|
+
source.addEventListener(type, event => emit(JSON.parse(event.data)));
|
|
128
58
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return
|
|
59
|
+
return () => source.close();
|
|
60
|
+
};
|
|
61
|
+
return {
|
|
62
|
+
'ui.notify': () => undefined,
|
|
63
|
+
'ui.setCommands': () => undefined,
|
|
64
|
+
'ui.setMenu': () => undefined,
|
|
65
|
+
'ui.setActiveTab': () => undefined,
|
|
66
|
+
'ui.shortcut': () => undefined,
|
|
67
|
+
'bff.request': request,
|
|
68
|
+
'events.graph': graphEvents,
|
|
69
|
+
'idb.get': idb.get,
|
|
70
|
+
'idb.put': idb.put,
|
|
71
|
+
'idb.delete': idb.delete,
|
|
72
|
+
'idb.query': idb.query,
|
|
73
|
+
'apps.list': unavailable,
|
|
74
|
+
'apps.get': unavailable,
|
|
75
|
+
'apps.save': unavailable,
|
|
76
|
+
'apps.delete': unavailable,
|
|
77
|
+
'apps.upload': unavailable,
|
|
78
|
+
'apps.launch': unavailable,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const DARK = '(prefers-color-scheme: dark)';
|
|
82
|
+
const osConfig = () => ({
|
|
83
|
+
theme: matchMedia(DARK).matches ? 'dark' : 'light',
|
|
84
|
+
language: navigator.language.split('-')[0] ?? 'en',
|
|
85
|
+
tabId: 'dev',
|
|
86
|
+
windowId: 'dev',
|
|
87
|
+
instanceId: null,
|
|
88
|
+
});
|
|
89
|
+
/**
|
|
90
|
+
* Stands in for the host while the app runs standalone in development: `connect()` reaches it
|
|
91
|
+
* the way it reaches the host, and BFF calls go through the dev-auth proxy.
|
|
92
|
+
*/
|
|
93
|
+
export function installDevHost(options) {
|
|
94
|
+
const handlers = devHandlers(options);
|
|
95
|
+
const context = () => ({ appId: options.appId, host: 'dev', osConfig: osConfig() });
|
|
96
|
+
let connection;
|
|
97
|
+
const stopAccepting = acceptApp({
|
|
98
|
+
source: () => window,
|
|
99
|
+
origin: location.origin,
|
|
100
|
+
context,
|
|
101
|
+
onConnect: port => {
|
|
102
|
+
connection?.close();
|
|
103
|
+
connection = serve(port, handlers);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
const scheme = matchMedia(DARK);
|
|
107
|
+
const onSchemeChange = () => connection?.emit('context.changed', context());
|
|
108
|
+
scheme.addEventListener('change', onSchemeChange);
|
|
109
|
+
return () => {
|
|
110
|
+
stopAccepting();
|
|
111
|
+
scheme.removeEventListener('change', onSchemeChange);
|
|
112
|
+
connection?.close();
|
|
113
|
+
};
|
|
132
114
|
}
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
export type ErrorCode = 'invalid' | 'unauthenticated' | 'forbidden' | 'not_found' | 'conflict' | 'rate_limited' | 'unavailable' | 'timeout' | 'cancelled' | 'protocol' | 'internal';
|
|
2
|
+
/** An error as it crosses the wire. */
|
|
3
|
+
export interface WireError {
|
|
4
|
+
code: ErrorCode;
|
|
5
|
+
message: string;
|
|
6
|
+
status?: number;
|
|
7
|
+
details?: unknown;
|
|
7
8
|
}
|
|
8
|
-
/**
|
|
9
|
-
export declare class
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export declare class EntityNotFoundError extends SdkError {
|
|
26
|
-
constructor(id: string);
|
|
9
|
+
/** The one error every SDK and host operation rejects with. */
|
|
10
|
+
export declare class BardiocError extends Error {
|
|
11
|
+
readonly code: ErrorCode;
|
|
12
|
+
readonly name = "BardiocError";
|
|
13
|
+
readonly status?: number;
|
|
14
|
+
readonly details?: unknown;
|
|
15
|
+
readonly method?: string;
|
|
16
|
+
constructor(code: ErrorCode, message: string, options?: {
|
|
17
|
+
status?: number;
|
|
18
|
+
details?: unknown;
|
|
19
|
+
method?: string;
|
|
20
|
+
});
|
|
21
|
+
/** Classifies an HTTP failure by its status. */
|
|
22
|
+
static fromStatus(status: number, message: string, details?: unknown): BardiocError;
|
|
23
|
+
static from(error: unknown): BardiocError;
|
|
24
|
+
static fromWire({ code, message, status, details }: WireError, method?: string): BardiocError;
|
|
25
|
+
toWire(): WireError;
|
|
27
26
|
}
|
package/dist/errors.js
CHANGED
|
@@ -1,57 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
const STATUS_CODES = {
|
|
2
|
+
400: 'invalid',
|
|
3
|
+
401: 'unauthenticated',
|
|
4
|
+
403: 'forbidden',
|
|
5
|
+
404: 'not_found',
|
|
6
|
+
408: 'timeout',
|
|
7
|
+
409: 'conflict',
|
|
8
|
+
422: 'invalid',
|
|
9
|
+
429: 'rate_limited',
|
|
10
|
+
504: 'timeout',
|
|
11
|
+
};
|
|
12
|
+
/** The one error every SDK and host operation rejects with. */
|
|
13
|
+
export class BardiocError extends Error {
|
|
3
14
|
code;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
15
|
+
name = 'BardiocError';
|
|
16
|
+
status;
|
|
17
|
+
details;
|
|
18
|
+
method;
|
|
19
|
+
constructor(code, message, options = {}) {
|
|
7
20
|
super(message);
|
|
8
21
|
this.code = code;
|
|
9
|
-
this.
|
|
10
|
-
this.
|
|
11
|
-
this.
|
|
12
|
-
Object.setPrototypeOf(this, SdkError.prototype);
|
|
22
|
+
this.status = options.status;
|
|
23
|
+
this.details = options.details;
|
|
24
|
+
this.method = options.method;
|
|
13
25
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
super(`Operation '${operation}' timed out after ${timeoutMs}ms`, 'TIMEOUT', undefined, {
|
|
19
|
-
operation,
|
|
20
|
-
timeoutMs,
|
|
21
|
-
});
|
|
22
|
-
this.name = 'TimeoutError';
|
|
23
|
-
Object.setPrototypeOf(this, TimeoutError.prototype);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
/** Network/HTTP error (4xx, 5xx) */
|
|
27
|
-
export class NetworkError extends SdkError {
|
|
28
|
-
constructor(message, statusCode) {
|
|
29
|
-
super(message, 'NETWORK_ERROR', statusCode);
|
|
30
|
-
this.name = 'NetworkError';
|
|
31
|
-
Object.setPrototypeOf(this, NetworkError.prototype);
|
|
26
|
+
/** Classifies an HTTP failure by its status. */
|
|
27
|
+
static fromStatus(status, message, details) {
|
|
28
|
+
const code = STATUS_CODES[status] ?? (status >= 500 ? 'unavailable' : 'invalid');
|
|
29
|
+
return new BardiocError(code, message, { status, details });
|
|
32
30
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
super(message, 'VALIDATION_ERROR', 400, field ? { field } : undefined);
|
|
38
|
-
this.name = 'ValidationError';
|
|
39
|
-
Object.setPrototypeOf(this, ValidationError.prototype);
|
|
31
|
+
static from(error) {
|
|
32
|
+
if (error instanceof BardiocError)
|
|
33
|
+
return error;
|
|
34
|
+
return new BardiocError('internal', error instanceof Error ? error.message : String(error));
|
|
40
35
|
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export class PermissionError extends SdkError {
|
|
44
|
-
constructor(permission) {
|
|
45
|
-
super(`Missing required permission: ${permission}`, 'PERMISSION_ERROR', 403, { permission });
|
|
46
|
-
this.name = 'PermissionError';
|
|
47
|
-
Object.setPrototypeOf(this, PermissionError.prototype);
|
|
36
|
+
static fromWire({ code, message, status, details }, method) {
|
|
37
|
+
return new BardiocError(code, message, { status, details, method });
|
|
48
38
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
export class EntityNotFoundError extends SdkError {
|
|
52
|
-
constructor(id) {
|
|
53
|
-
super(`Entity not found: ${id}`, 'ENTITY_NOT_FOUND', 404, { id });
|
|
54
|
-
this.name = 'EntityNotFoundError';
|
|
55
|
-
Object.setPrototypeOf(this, EntityNotFoundError.prototype);
|
|
39
|
+
toWire() {
|
|
40
|
+
return { code: this.code, message: this.message, status: this.status, details: this.details };
|
|
56
41
|
}
|
|
57
42
|
}
|