@agentshouse/app-bridge 0.1.0-alpha.9 → 0.1.0-c2b8788fb515c
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/LICENSE +21 -0
- package/README.md +11 -74
- package/{client → dist/client}/src/bridge.d.ts +16 -5
- package/{client → dist/client}/src/bridge.js +89 -25
- package/dist/client/src/index.d.ts +3 -0
- package/{client → dist/client}/src/test-host.d.ts +9 -5
- package/{client → dist/client}/src/test-host.js +30 -4
- package/{client → dist/client}/src/wire.d.ts +25 -6
- package/{src → dist/src}/apps/remainder.js +1 -1
- package/docs/families.md +104 -0
- package/package.json +14 -14
- package/client/src/index.d.ts +0 -3
- /package/{client → dist/client}/src/index.js +0 -0
- /package/{client → dist/client}/src/wire.js +0 -0
- /package/{src → dist/src}/apps/remainder.d.ts +0 -0
- /package/{src → dist/src}/apps/tab.d.ts +0 -0
- /package/{src → dist/src}/apps/tab.js +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anton (trof3039)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,69 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
The renderer-side client of the agents.house App bridge. An App served by House
|
|
4
4
|
receives one message port before its own scripts run; this package is that
|
|
5
|
-
port's one listener. It carries
|
|
5
|
+
port's one listener. It carries eleven families: navigation, ceremonies,
|
|
6
|
+
installation, notifications, status, Site image reads, App release code,
|
|
7
|
+
external calls, App messages and App and Site screenshot reads.
|
|
8
|
+
[docs/families.md](docs/families.md) describes each and the test host.
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
sets its tab's title and icon with `setTab({ title, icon })`, either or both:
|
|
11
|
-
a title of at most 200 characters and an icon given as a base64 `data:` URL
|
|
12
|
-
of an SVG, PNG, GIF or WebP image of at most 1,048,576 characters. The shell
|
|
13
|
-
shows them over the App's display name, or a Site's title, and its tab icon
|
|
14
|
-
until the next load, and ignores a request outside those bounds.
|
|
15
|
-
- **Ceremony** - the App asks the shell to leave for one of House's own
|
|
16
|
-
purpose-bound authenticated pages. The shell navigates only to a same-origin
|
|
17
|
-
absolute path, carries its current address as the return target, and the page
|
|
18
|
-
returns there when it completes. An App names an address House gave it in a
|
|
19
|
-
reading and composes none.
|
|
20
|
-
- **Installation** - `install()` offers the page as a Home-screen app. Where the
|
|
21
|
-
browser has an install prompt ready the shell shows it and the call resolves
|
|
22
|
-
`'accepted'` or `'dismissed'` with the User's choice, spending that prompt;
|
|
23
|
-
otherwise, as on every iPhone and iPad, it resolves `'manual'` and the App
|
|
24
|
-
shows its own instructions. A prompt the browser refuses to show, as for a
|
|
25
|
-
call made without a click, rejects with the browser's own `DOMException`
|
|
26
|
-
and stays ready. Call it from a click. The standard
|
|
27
|
-
`display-mode: standalone` media query tells the App it runs installed.
|
|
28
|
-
- **Status** - the App reference the Runtime port is attached under, sync
|
|
29
|
-
state, credential state, release availability and observed failure, `ga`:
|
|
30
|
-
whether the page measures and the visitor's stored analytics choice, and
|
|
31
|
-
`service_mode`: `open`, `write-halted` while House refuses writes, so the
|
|
32
|
-
App can stop retrying them, or `null` while the runtime holds no House
|
|
33
|
-
connection.
|
|
34
|
-
The App reports the visitor's choice with `gaConsent('accept')` or
|
|
35
|
-
`gaConsent('decline')`; the shell hands it to House's analytics loader, and
|
|
36
|
-
the choice never reaches House.
|
|
37
|
-
- **Site image read** - given a Published Room and an original attachment
|
|
38
|
-
reference, it answers the web copy's path and metadata, or `null` when no
|
|
39
|
-
copy exists. The original bytes never cross this bridge at a Site.
|
|
40
|
-
- **App release code** - `sourceTop(app, release)` answers the top level of
|
|
41
|
-
the latest successful release of an App the User may use: its folders, its
|
|
42
|
-
files with their sizes and its root README's text, or `null` for none;
|
|
43
|
-
`sourceDownload(app, release)` answers every file of that release with its
|
|
44
|
-
path, its size and its `text`, or its bytes as `base64`. The browser keeps
|
|
45
|
-
each answer per App and release: a read naming a release it holds answers
|
|
46
|
-
without asking House, and keeping a newer release drops the earlier one. A
|
|
47
|
-
refusal is House's `{ error: { code } }`, `app_release_not_found` for an
|
|
48
|
-
App with no release; a read House does not answer, or one that outlived
|
|
49
|
-
the browser's signed-in User, is `null`.
|
|
50
|
-
- **App screenshot read** - given an App reference, `readScreenshot(app)`
|
|
51
|
-
answers the image this browser captured of that App's screen the first time
|
|
52
|
-
it ran its current release, as a PNG `Blob` at most 1200 pixels wide, or
|
|
53
|
-
`null` when this browser holds none. The image stays in the browser; a Site
|
|
54
|
-
holds none.
|
|
55
|
-
|
|
56
|
-
The package carries no credential handling and no Runtime family. It surfaces
|
|
57
|
-
the dedicated Runtime port the worker transfers over the bridge as a plain
|
|
58
|
-
message port, which the App hands to the MD Runtime client entry.
|
|
10
|
+
The package carries no credential handling and no Runtime family. House builds
|
|
11
|
+
the MD Runtime client into the page beside the bridge, and `bridge.runtime`
|
|
12
|
+
resolves to that client, so the App imports and constructs none.
|
|
59
13
|
|
|
60
14
|
## Use
|
|
61
15
|
|
|
62
16
|
```js
|
|
63
17
|
import { connectHouseBridge } from '@agentshouse/app-bridge';
|
|
64
|
-
import { connectMdRuntime } from '@agentshouse/mdruntime/client';
|
|
65
18
|
|
|
66
19
|
const bridge = connectHouseBridge();
|
|
67
|
-
const md =
|
|
20
|
+
const md = await bridge.runtime;
|
|
68
21
|
|
|
69
22
|
bridge.onAddress((address) => render(address));
|
|
70
23
|
bridge.onStatus((status) => showSync(status.sync));
|
|
@@ -84,9 +37,8 @@ async function manageSecurity(securityPath) {
|
|
|
84
37
|
}
|
|
85
38
|
```
|
|
86
39
|
|
|
87
|
-
`bridge.runtime` resolves
|
|
88
|
-
|
|
89
|
-
directly at run time and casts at the type level.
|
|
40
|
+
`bridge.runtime` resolves to the `MdRuntimeClient` of
|
|
41
|
+
`@agentshouse/mdruntime/client`, whose README describes its members.
|
|
90
42
|
|
|
91
43
|
## Test without a House
|
|
92
44
|
|
|
@@ -95,23 +47,8 @@ import { connectHouseBridge } from '@agentshouse/app-bridge';
|
|
|
95
47
|
import { createHouseBridgeTestHost } from '@agentshouse/app-bridge/test-host';
|
|
96
48
|
|
|
97
49
|
const host = createHouseBridgeTestHost({ address: '/rooms/atlas' });
|
|
98
|
-
const bridge = connectHouseBridge(host
|
|
50
|
+
const bridge = connectHouseBridge(host);
|
|
99
51
|
|
|
100
52
|
bridge.push('/rooms/atlas/members');
|
|
101
53
|
host.open('/rooms/atlas');
|
|
102
54
|
```
|
|
103
|
-
|
|
104
|
-
The host plays the shell and the worker: it delivers the address the App starts
|
|
105
|
-
at and each address `open` plays as Back or Forward through the shell's own
|
|
106
|
-
slice below the App's path, admitting nothing, so it delivers `//notes` exactly
|
|
107
|
-
as a browser at `/apps/<app>//notes` does. It records every push and replace
|
|
108
|
-
and answers each with the address the shell would then show, records an
|
|
109
|
-
admitted ceremony, refuses a push, replace or ceremony that is not a
|
|
110
|
-
same-origin absolute path, publishes a status, its `service_mode` included,
|
|
111
|
-
records in `consents` each
|
|
112
|
-
`gaConsent` it takes while that status measures and publishes the status the
|
|
113
|
-
choice leaves, records in `tabs` each `setTab` the shell would take and ignores
|
|
114
|
-
any other, answers a release code read from the `sources` it was given, or
|
|
115
|
-
`null`, answers `readScreenshot` from the `screenshots` it was given by App
|
|
116
|
-
reference, answers every `install()` `'manual'`, as a browser with no install
|
|
117
|
-
prompt does, and attaches a Runtime port.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { MdRuntimeClient } from '@agentshouse/mdruntime/client';
|
|
2
|
+
import type { HouseAppMessage, HouseExternalAnswer, HouseExternalRequest, HouseBridgePort, HouseGaChoice, HouseRefusal, HouseSourceDownload, HouseSourceTop, HouseStatus, HouseTab } from './wire.js';
|
|
2
3
|
export declare class HouseCeremonyRefused extends Error {
|
|
3
4
|
readonly path: string | null;
|
|
4
5
|
constructor(path: string | null);
|
|
@@ -14,22 +15,32 @@ export interface HouseImageCopy {
|
|
|
14
15
|
readonly media_type: string;
|
|
15
16
|
}
|
|
16
17
|
type HouseInstallation = 'accepted' | 'dismissed' | 'manual';
|
|
18
|
+
type HouseNotifications = 'on' | 'off' | 'blocked' | 'unavailable';
|
|
19
|
+
export interface HouseBridgeSource {
|
|
20
|
+
readonly port: Promise<HouseBridgePort>;
|
|
21
|
+
readonly runtime: Promise<MdRuntimeClient>;
|
|
22
|
+
}
|
|
17
23
|
export interface HouseBridge {
|
|
18
|
-
readonly runtime: Promise<
|
|
24
|
+
readonly runtime: Promise<MdRuntimeClient>;
|
|
19
25
|
address(): string | null;
|
|
20
26
|
onAddress(listener: (address: string) => void): () => void;
|
|
21
27
|
push(address: string): Promise<string>;
|
|
22
28
|
replace(address: string): Promise<string>;
|
|
23
29
|
ceremony(path: string): Promise<never>;
|
|
24
30
|
install(): Promise<HouseInstallation>;
|
|
31
|
+
notifications(on?: boolean): Promise<HouseNotifications>;
|
|
32
|
+
setBadge(unread: readonly string[]): Promise<void>;
|
|
25
33
|
setTab(tab: HouseTab): void;
|
|
26
34
|
readAttachment(room: string, attachment: string): Promise<HouseImageCopy | null>;
|
|
27
|
-
sourceTop(app: string, release?: string): Promise<HouseSourceTop |
|
|
28
|
-
sourceDownload(app: string, release?: string): Promise<HouseSourceDownload |
|
|
35
|
+
sourceTop(app: string, release?: string): Promise<HouseSourceTop | HouseRefusal | null>;
|
|
36
|
+
sourceDownload(app: string, release?: string): Promise<HouseSourceDownload | HouseRefusal | null>;
|
|
37
|
+
callExternal(request: HouseExternalRequest): Promise<HouseExternalAnswer | HouseRefusal | null>;
|
|
29
38
|
readScreenshot(app: string): Promise<Blob | null>;
|
|
39
|
+
readSiteScreenshot(site: string): Promise<Blob | null>;
|
|
30
40
|
status(): HouseStatus | null;
|
|
31
41
|
onStatus(listener: (status: HouseStatus) => void): () => void;
|
|
42
|
+
messages(listener: (message: HouseAppMessage) => void): () => void;
|
|
32
43
|
gaConsent(choice: HouseGaChoice): void;
|
|
33
44
|
}
|
|
34
|
-
export declare function connectHouseBridge(source?:
|
|
45
|
+
export declare function connectHouseBridge(source?: HouseBridgeSource): HouseBridge;
|
|
35
46
|
export {};
|
|
@@ -14,6 +14,20 @@ export class HouseNavigationRefused extends Error {
|
|
|
14
14
|
this.address = address;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
function answered(pending, answer, value) {
|
|
18
|
+
if (typeof answer.id !== 'number')
|
|
19
|
+
return;
|
|
20
|
+
const asked = pending.get(answer.id);
|
|
21
|
+
if (asked === undefined)
|
|
22
|
+
return;
|
|
23
|
+
pending.delete(answer.id);
|
|
24
|
+
if (answer.refused === true)
|
|
25
|
+
asked.reject(new HouseCeremonyRefused(null));
|
|
26
|
+
else if (answer.refusal !== undefined)
|
|
27
|
+
asked.reject(new DOMException(answer.refusal.message, answer.refusal.name));
|
|
28
|
+
else
|
|
29
|
+
asked.resolve(value);
|
|
30
|
+
}
|
|
17
31
|
function settled() {
|
|
18
32
|
let settle = null;
|
|
19
33
|
const promise = new Promise((resolve, reject) => {
|
|
@@ -22,13 +36,12 @@ function settled() {
|
|
|
22
36
|
return { promise, settle: settle };
|
|
23
37
|
}
|
|
24
38
|
function heldBridge() {
|
|
25
|
-
const held = globalThis
|
|
26
|
-
if (held === undefined)
|
|
39
|
+
const held = globalThis;
|
|
40
|
+
if (held.houseBridge === undefined || held.houseRuntime === undefined)
|
|
27
41
|
throw new Error('house_bridge_absent');
|
|
28
|
-
return held;
|
|
42
|
+
return { port: held.houseBridge, runtime: held.houseRuntime };
|
|
29
43
|
}
|
|
30
44
|
export function connectHouseBridge(source = heldBridge()) {
|
|
31
|
-
const runtime = settled();
|
|
32
45
|
const ceremonies = [];
|
|
33
46
|
const navigations = [];
|
|
34
47
|
const attachments = new Map();
|
|
@@ -37,10 +50,17 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
37
50
|
let nextSource = 0;
|
|
38
51
|
const installations = new Map();
|
|
39
52
|
let nextInstallation = 0;
|
|
53
|
+
const notifications = new Map();
|
|
54
|
+
let nextNotifications = 0;
|
|
55
|
+
const badges = new Map();
|
|
56
|
+
let nextBadge = 0;
|
|
40
57
|
const screenshots = new Map();
|
|
41
58
|
let nextScreenshot = 0;
|
|
59
|
+
const externals = new Map();
|
|
60
|
+
let nextExternal = 0;
|
|
42
61
|
const addressListeners = new Set();
|
|
43
62
|
const statusListeners = new Set();
|
|
63
|
+
const messageListeners = new Set();
|
|
44
64
|
let outbox = [];
|
|
45
65
|
let held = null;
|
|
46
66
|
let address = null;
|
|
@@ -55,18 +75,18 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
55
75
|
const data = event.data;
|
|
56
76
|
if (data === null || typeof data !== 'object')
|
|
57
77
|
return;
|
|
58
|
-
if (data.house === 'runtime') {
|
|
59
|
-
const [port] = event.ports;
|
|
60
|
-
if (port !== undefined)
|
|
61
|
-
runtime.settle.resolve(port);
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
78
|
if (data.house === 'status') {
|
|
65
79
|
status = data.status;
|
|
66
80
|
for (const listener of statusListeners)
|
|
67
81
|
listener(status);
|
|
68
82
|
return;
|
|
69
83
|
}
|
|
84
|
+
if (data.house === 'app-message') {
|
|
85
|
+
const { from, body } = data;
|
|
86
|
+
for (const listener of messageListeners)
|
|
87
|
+
listener({ from, body });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
70
90
|
if (data.house === 'address') {
|
|
71
91
|
address = String(data.address);
|
|
72
92
|
if (data.navigated === true)
|
|
@@ -95,27 +115,38 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
95
115
|
return;
|
|
96
116
|
}
|
|
97
117
|
if (data.house === 'install') {
|
|
118
|
+
const answer = data;
|
|
119
|
+
answered(installations, answer, answer.outcome);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (data.house === 'notifications') {
|
|
123
|
+
const answer = data;
|
|
124
|
+
answered(notifications, answer, answer.state);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (data.house === 'badge') {
|
|
128
|
+
answered(badges, data, undefined);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (data.house === 'source') {
|
|
98
132
|
const answer = data;
|
|
99
133
|
if (typeof answer.id !== 'number')
|
|
100
134
|
return;
|
|
101
|
-
const pending =
|
|
135
|
+
const pending = sources.get(answer.id);
|
|
102
136
|
if (pending === undefined)
|
|
103
137
|
return;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
pending.resolve(answer.outcome);
|
|
107
|
-
else
|
|
108
|
-
pending.reject(new DOMException(answer.refusal?.message, answer.refusal?.name));
|
|
138
|
+
sources.delete(answer.id);
|
|
139
|
+
pending.resolve(answer.answer ?? null);
|
|
109
140
|
return;
|
|
110
141
|
}
|
|
111
|
-
if (data.house === '
|
|
142
|
+
if (data.house === 'external-call') {
|
|
112
143
|
const answer = data;
|
|
113
144
|
if (typeof answer.id !== 'number')
|
|
114
145
|
return;
|
|
115
|
-
const pending =
|
|
146
|
+
const pending = externals.get(answer.id);
|
|
116
147
|
if (pending === undefined)
|
|
117
148
|
return;
|
|
118
|
-
|
|
149
|
+
externals.delete(answer.id);
|
|
119
150
|
pending.resolve(answer.answer ?? null);
|
|
120
151
|
return;
|
|
121
152
|
}
|
|
@@ -138,7 +169,7 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
138
169
|
pending?.settle.reject(new HouseCeremonyRefused(named));
|
|
139
170
|
}
|
|
140
171
|
}
|
|
141
|
-
void source.then((port) => {
|
|
172
|
+
void source.port.then((port) => {
|
|
142
173
|
held = port;
|
|
143
174
|
port.addEventListener('message', receive);
|
|
144
175
|
port.start();
|
|
@@ -154,6 +185,13 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
154
185
|
send({ house, id, app, release });
|
|
155
186
|
return pending.promise;
|
|
156
187
|
}
|
|
188
|
+
function screenshot(house, asked) {
|
|
189
|
+
const pending = settled();
|
|
190
|
+
const id = ++nextScreenshot;
|
|
191
|
+
screenshots.set(id, pending.settle);
|
|
192
|
+
send({ house, id, ...asked });
|
|
193
|
+
return pending.promise;
|
|
194
|
+
}
|
|
157
195
|
function navigate(mode, asked) {
|
|
158
196
|
const pending = settled();
|
|
159
197
|
navigations.push({ address: asked, settle: pending.settle });
|
|
@@ -161,7 +199,7 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
161
199
|
return pending.promise;
|
|
162
200
|
}
|
|
163
201
|
return {
|
|
164
|
-
runtime: runtime
|
|
202
|
+
runtime: source.runtime,
|
|
165
203
|
address() {
|
|
166
204
|
return address;
|
|
167
205
|
},
|
|
@@ -190,6 +228,20 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
190
228
|
send({ house: 'install', id });
|
|
191
229
|
return pending.promise;
|
|
192
230
|
},
|
|
231
|
+
notifications(on) {
|
|
232
|
+
const pending = settled();
|
|
233
|
+
const id = ++nextNotifications;
|
|
234
|
+
notifications.set(id, pending.settle);
|
|
235
|
+
send({ house: 'notifications', id, on });
|
|
236
|
+
return pending.promise;
|
|
237
|
+
},
|
|
238
|
+
setBadge(unread) {
|
|
239
|
+
const pending = settled();
|
|
240
|
+
const id = ++nextBadge;
|
|
241
|
+
badges.set(id, pending.settle);
|
|
242
|
+
send({ house: 'badge', id, unread });
|
|
243
|
+
return pending.promise;
|
|
244
|
+
},
|
|
193
245
|
setTab(tab) {
|
|
194
246
|
send({ house: 'tab', title: tab.title, icon: tab.icon });
|
|
195
247
|
},
|
|
@@ -206,13 +258,19 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
206
258
|
sourceDownload(app, release) {
|
|
207
259
|
return readSource('source-download', app, release);
|
|
208
260
|
},
|
|
209
|
-
|
|
261
|
+
callExternal(request) {
|
|
210
262
|
const pending = settled();
|
|
211
|
-
const id = ++
|
|
212
|
-
|
|
213
|
-
send({ house: '
|
|
263
|
+
const id = ++nextExternal;
|
|
264
|
+
externals.set(id, pending.settle);
|
|
265
|
+
send({ house: 'external-call', id, request });
|
|
214
266
|
return pending.promise;
|
|
215
267
|
},
|
|
268
|
+
readScreenshot(app) {
|
|
269
|
+
return screenshot('screenshot-read', { app });
|
|
270
|
+
},
|
|
271
|
+
readSiteScreenshot(site) {
|
|
272
|
+
return screenshot('site-screenshot-read', { site });
|
|
273
|
+
},
|
|
216
274
|
status() {
|
|
217
275
|
return status;
|
|
218
276
|
},
|
|
@@ -222,6 +280,12 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
222
280
|
statusListeners.delete(listener);
|
|
223
281
|
};
|
|
224
282
|
},
|
|
283
|
+
messages(listener) {
|
|
284
|
+
messageListeners.add(listener);
|
|
285
|
+
return () => {
|
|
286
|
+
messageListeners.delete(listener);
|
|
287
|
+
};
|
|
288
|
+
},
|
|
225
289
|
gaConsent(choice) {
|
|
226
290
|
send({ house: 'ga/consent', choice });
|
|
227
291
|
},
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { connectHouseBridge, HouseCeremonyRefused, HouseNavigationRefused, type HouseBridge, type HouseBridgeSource, type HouseImageCopy, } from './bridge.js';
|
|
2
|
+
export { HOUSE_ADDRESS_MAX_CHARS } from './wire.js';
|
|
3
|
+
export type { HouseAppMessage, HouseBridgeEvent, HouseBridgePort, HouseCredentialState, HouseExternalAnswer, HouseExternalRequest, HouseGa, HouseGaChoice, HouseNavigationMode, HouseRefusal, HouseSourceDownload, HouseSourceFile, HouseSourceTop, HouseStatus, HouseSyncState, } from './wire.js';
|
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { MdRuntimeClient } from '@agentshouse/mdruntime/client';
|
|
2
|
+
import type { HouseBridgeSource, HouseImageCopy } from './bridge.js';
|
|
3
|
+
import type { HouseAppMessage, HouseExternalAnswer, HouseExternalRequest, HouseGaChoice, HouseNavigationMode, HouseRefusal, HouseSourceDownload, HouseSourceTop, HouseStatus, HouseTab } from './wire.js';
|
|
3
4
|
export interface HouseBridgeNavigation {
|
|
4
5
|
readonly mode: HouseNavigationMode;
|
|
5
6
|
readonly address: string;
|
|
6
7
|
}
|
|
7
|
-
export interface HouseBridgeTestHost {
|
|
8
|
-
readonly port: Promise<HouseBridgePort>;
|
|
8
|
+
export interface HouseBridgeTestHost extends HouseBridgeSource {
|
|
9
9
|
readonly navigations: readonly HouseBridgeNavigation[];
|
|
10
10
|
readonly ceremonies: readonly string[];
|
|
11
11
|
readonly consents: readonly HouseGaChoice[];
|
|
12
12
|
readonly tabs: readonly HouseTab[];
|
|
13
|
+
readonly externalCalls: readonly HouseExternalRequest[];
|
|
13
14
|
address(): string;
|
|
14
15
|
open(address: string): void;
|
|
15
16
|
publish(status: HouseStatus): void;
|
|
16
|
-
|
|
17
|
+
message(message: HouseAppMessage): void;
|
|
18
|
+
attachRuntime(runtime: MdRuntimeClient): void;
|
|
17
19
|
}
|
|
18
20
|
export interface HouseBridgeTestHostOptions {
|
|
19
21
|
readonly address?: string;
|
|
@@ -26,5 +28,7 @@ export interface HouseBridgeTestHostOptions {
|
|
|
26
28
|
readonly download: HouseSourceDownload;
|
|
27
29
|
}[];
|
|
28
30
|
readonly screenshots?: Readonly<Record<string, Blob>>;
|
|
31
|
+
readonly siteScreenshots?: Readonly<Record<string, Blob>>;
|
|
32
|
+
readonly external?: (request: HouseExternalRequest) => HouseExternalAnswer | HouseRefusal | null | Promise<HouseExternalAnswer | HouseRefusal | null>;
|
|
29
33
|
}
|
|
30
34
|
export declare function createHouseBridgeTestHost(options?: HouseBridgeTestHostOptions): HouseBridgeTestHost;
|
|
@@ -17,13 +17,18 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
17
17
|
const ceremonies = [];
|
|
18
18
|
const consents = [];
|
|
19
19
|
const tabs = [];
|
|
20
|
+
const externalCalls = [];
|
|
20
21
|
const listeners = new Set();
|
|
21
22
|
const queued = [];
|
|
22
23
|
let started = false;
|
|
23
24
|
let address = appAddressOf('/', options.address ?? '/');
|
|
24
25
|
let status = options.status ?? null;
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
let attached = () => { };
|
|
27
|
+
const runtime = new Promise((resolve) => {
|
|
28
|
+
attached = resolve;
|
|
29
|
+
});
|
|
30
|
+
function deliver(data) {
|
|
31
|
+
const event = { data };
|
|
27
32
|
if (!started) {
|
|
28
33
|
queued.push(event);
|
|
29
34
|
return;
|
|
@@ -82,6 +87,12 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
82
87
|
else if (data.house === 'install') {
|
|
83
88
|
deliver({ house: 'install', id: data.id, outcome: 'manual' });
|
|
84
89
|
}
|
|
90
|
+
else if (data.house === 'notifications') {
|
|
91
|
+
deliver({ house: 'notifications', id: data.id, state: 'unavailable' });
|
|
92
|
+
}
|
|
93
|
+
else if (data.house === 'badge') {
|
|
94
|
+
deliver({ house: 'badge', id: data.id });
|
|
95
|
+
}
|
|
85
96
|
else if (data.house === 'tab') {
|
|
86
97
|
const change = tabChange(data);
|
|
87
98
|
if (change !== null)
|
|
@@ -112,11 +123,21 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
112
123
|
},
|
|
113
124
|
});
|
|
114
125
|
}
|
|
126
|
+
else if (data.house === 'external-call') {
|
|
127
|
+
const asked = data;
|
|
128
|
+
externalCalls.push(asked.request);
|
|
129
|
+
void Promise.resolve(options.external?.(asked.request) ?? null).then((answer) => deliver({ house: 'external-call', id: asked.id, answer }));
|
|
130
|
+
}
|
|
115
131
|
else if (data.house === 'screenshot-read') {
|
|
116
132
|
const asked = data;
|
|
117
133
|
const held = typeof asked.app === 'string' ? options.screenshots?.[asked.app] : undefined;
|
|
118
134
|
deliver({ house: 'screenshot', id: asked.id, image: held ?? null });
|
|
119
135
|
}
|
|
136
|
+
else if (data.house === 'site-screenshot-read') {
|
|
137
|
+
const asked = data;
|
|
138
|
+
const held = typeof asked.site === 'string' ? options.siteScreenshots?.[asked.site] : undefined;
|
|
139
|
+
deliver({ house: 'screenshot', id: asked.id, image: held ?? null });
|
|
140
|
+
}
|
|
120
141
|
}
|
|
121
142
|
const port = {
|
|
122
143
|
postMessage(message) {
|
|
@@ -141,10 +162,12 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
141
162
|
deliver({ house: 'address', address, navigated: false });
|
|
142
163
|
return {
|
|
143
164
|
port: Promise.resolve(port),
|
|
165
|
+
runtime,
|
|
144
166
|
navigations,
|
|
145
167
|
ceremonies,
|
|
146
168
|
consents,
|
|
147
169
|
tabs,
|
|
170
|
+
externalCalls,
|
|
148
171
|
address() {
|
|
149
172
|
return address;
|
|
150
173
|
},
|
|
@@ -152,8 +175,11 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
152
175
|
arrive(appAddressOf('/', asked), false);
|
|
153
176
|
},
|
|
154
177
|
publish,
|
|
155
|
-
|
|
156
|
-
deliver({ house: '
|
|
178
|
+
message(message) {
|
|
179
|
+
deliver({ house: 'app-message', from: message.from, body: message.body });
|
|
180
|
+
},
|
|
181
|
+
attachRuntime(client) {
|
|
182
|
+
attached(client);
|
|
157
183
|
},
|
|
158
184
|
};
|
|
159
185
|
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import type { TabChange } from '../../src/apps/tab.js';
|
|
2
2
|
export interface HouseBridgeEvent {
|
|
3
3
|
readonly data: unknown;
|
|
4
|
-
readonly ports: readonly HouseMessagePort[];
|
|
5
4
|
}
|
|
6
|
-
export interface
|
|
5
|
+
export interface HouseBridgePort {
|
|
7
6
|
postMessage(message: unknown): void;
|
|
8
7
|
addEventListener(type: 'message', listener: (event: HouseBridgeEvent) => void): void;
|
|
9
8
|
start(): void;
|
|
10
9
|
}
|
|
11
|
-
export type HouseBridgePort = HouseMessagePort;
|
|
12
10
|
export interface HouseSyncState {
|
|
13
11
|
readonly state: string;
|
|
14
12
|
readonly providerUnreachable: boolean;
|
|
@@ -31,9 +29,9 @@ export interface HouseStatus {
|
|
|
31
29
|
readonly reason: string;
|
|
32
30
|
} | null;
|
|
33
31
|
readonly loss: string | null;
|
|
34
|
-
readonly release: unknown;
|
|
35
32
|
readonly ga: HouseGa;
|
|
36
33
|
readonly service_mode: 'open' | 'write-halted' | null;
|
|
34
|
+
readonly offline: boolean;
|
|
37
35
|
}
|
|
38
36
|
export type HouseNavigationMode = 'push' | 'replace';
|
|
39
37
|
interface HouseRelease {
|
|
@@ -59,11 +57,32 @@ export type HouseSourceFile = {
|
|
|
59
57
|
export interface HouseSourceDownload extends HouseRelease {
|
|
60
58
|
readonly files: readonly HouseSourceFile[];
|
|
61
59
|
}
|
|
62
|
-
export
|
|
60
|
+
export type HouseTab = TabChange;
|
|
61
|
+
export interface HouseExternalRequest {
|
|
62
|
+
readonly url: string;
|
|
63
|
+
readonly method?: string;
|
|
64
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
65
|
+
readonly body?: string;
|
|
66
|
+
readonly body_base64?: string;
|
|
67
|
+
readonly account?: string;
|
|
68
|
+
readonly slot?: string;
|
|
69
|
+
}
|
|
70
|
+
export type HouseExternalAnswer = {
|
|
71
|
+
readonly status: number;
|
|
72
|
+
readonly headers: Readonly<Record<string, string | readonly string[]>>;
|
|
73
|
+
} & ({
|
|
74
|
+
readonly body: string;
|
|
75
|
+
} | {
|
|
76
|
+
readonly body_base64: string;
|
|
77
|
+
});
|
|
78
|
+
export interface HouseRefusal {
|
|
63
79
|
readonly error: {
|
|
64
80
|
readonly code: string;
|
|
65
81
|
};
|
|
66
82
|
}
|
|
67
|
-
export
|
|
83
|
+
export interface HouseAppMessage {
|
|
84
|
+
readonly from: string;
|
|
85
|
+
readonly body: unknown;
|
|
86
|
+
}
|
|
68
87
|
export declare const HOUSE_ADDRESS_MAX_CHARS = 1000;
|
|
69
88
|
export {};
|
|
@@ -12,5 +12,5 @@ export function appAddressOf(root, address) {
|
|
|
12
12
|
export function shellAddressOf(root, appAddress) {
|
|
13
13
|
const { path, rest } = split(appAddress);
|
|
14
14
|
const below = path === '/' ? '' : path;
|
|
15
|
-
return `${root === '/' ? '' : root}${below}
|
|
15
|
+
return `${`${root === '/' ? '' : root}${below}` || '/'}${rest}`;
|
|
16
16
|
}
|
package/docs/families.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# The App bridge's families
|
|
2
|
+
|
|
3
|
+
- **Navigation** - the part of the shell's URL below the App's path, with its
|
|
4
|
+
query and fragment. The shell delivers it when the App starts and whenever the
|
|
5
|
+
User navigates; the App asks the shell to push or replace it. The App also
|
|
6
|
+
sets its tab's title and icon with `setTab({ title, icon })`, either or both:
|
|
7
|
+
a title of at most 200 characters and an icon given as a base64 `data:` URL
|
|
8
|
+
of an SVG, PNG, GIF or WebP image of at most 1,048,576 characters. The shell
|
|
9
|
+
shows them over the App's display name, or a Site's title, and its tab icon
|
|
10
|
+
until the next load, and ignores a request outside those bounds.
|
|
11
|
+
- **Ceremony** - the App asks the shell to leave for one of House's own
|
|
12
|
+
purpose-bound authenticated pages. The shell navigates only to a same-origin
|
|
13
|
+
absolute path, carries its current address as the return target, and the page
|
|
14
|
+
returns there when it completes. An App names an address House gave it in a
|
|
15
|
+
reading and composes none.
|
|
16
|
+
- **Installation** - `install()` offers the page as a Home-screen app. Where the
|
|
17
|
+
browser has an install prompt ready the shell shows it and the call resolves
|
|
18
|
+
`'accepted'` or `'dismissed'` with the User's choice, spending that prompt;
|
|
19
|
+
otherwise, as on every iPhone and iPad, it resolves `'manual'` and the App
|
|
20
|
+
shows its own instructions. A prompt the browser refuses to show, as for a
|
|
21
|
+
call made without a click, rejects with the browser's own `DOMException`
|
|
22
|
+
and stays ready. Call it from a click. The standard
|
|
23
|
+
`display-mode: standalone` media query tells the App it runs installed.
|
|
24
|
+
- **Notifications** - House's own panel alone; an App's or a Site's call
|
|
25
|
+
rejects with `HouseCeremonyRefused`. `notifications()` answers this browser
|
|
26
|
+
storage's push state: `'on'`, `'off'`, `'blocked'` where the browser refuses
|
|
27
|
+
notifications, or `'unavailable'` where it has no push, as on an iPhone or
|
|
28
|
+
iPad outside a Home-screen app. `notifications(true)` asks the browser's
|
|
29
|
+
permission and subscribes, `notifications(false)` unsubscribes, and each
|
|
30
|
+
answers the state it leaves; a failure of the browser's push service rejects
|
|
31
|
+
with the browser's own `DOMException`. Call `notifications(true)` from a
|
|
32
|
+
click. `setBadge(unread)` takes the identifiers of the Human Notifications
|
|
33
|
+
unread in this storage and sets the icon's badge to their number, or clears
|
|
34
|
+
it at none, where the browser shows one.
|
|
35
|
+
- **Status** - the App reference the runtime client is attached under, sync
|
|
36
|
+
state, credential state and observed failure, `ga`:
|
|
37
|
+
whether the page measures and the visitor's stored analytics choice, and
|
|
38
|
+
`service_mode`: `open`, `write-halted` while House refuses writes, so the
|
|
39
|
+
App can stop retrying them, or `null` while the runtime holds no House
|
|
40
|
+
connection.
|
|
41
|
+
The App reports the visitor's choice with `gaConsent('accept')` or
|
|
42
|
+
`gaConsent('decline')`; the shell hands it to House's analytics loader, and
|
|
43
|
+
the choice never reaches House.
|
|
44
|
+
- **Site image read** - given a Published Room and an original attachment
|
|
45
|
+
reference, it answers the web copy's path and metadata, or `null` when no
|
|
46
|
+
copy exists. The original bytes never cross this bridge at a Site.
|
|
47
|
+
- **App release code** - `sourceTop(app, release)` answers the top level of
|
|
48
|
+
the latest successful release of an App the User may use: its folders, its
|
|
49
|
+
files with their sizes and its root README's text, or `null` for none;
|
|
50
|
+
`sourceDownload(app, release)` answers every file of that release with its
|
|
51
|
+
path, its size and its `text`, or its bytes as `base64`. The browser keeps
|
|
52
|
+
each answer per App and release: a read naming a release it holds answers
|
|
53
|
+
without asking House, and keeping a newer release drops the earlier one. A
|
|
54
|
+
refusal is House's `{ error: { code } }`, `app_release_not_found` for an
|
|
55
|
+
App with no release; a read House does not answer, or one that outlived
|
|
56
|
+
the browser's signed-in User, is `null`.
|
|
57
|
+
- **External calls** - `callExternal({ url, method, headers, body, slot })`
|
|
58
|
+
has House's server send an HTTPS request to one of the release's declared
|
|
59
|
+
external origins and resolves with the service's `status`, `headers` and its
|
|
60
|
+
body as `body` text or `body_base64`, or a refusal as House's
|
|
61
|
+
`{ error: { code } }`, or `null` when House does not answer. House fills in the
|
|
62
|
+
App and its running release; naming an account slot places the key of the
|
|
63
|
+
account the User connected to it, which the App never sees, and naming none
|
|
64
|
+
sends no key. A Site's call names no slot, and the House panel names an
|
|
65
|
+
`account` instead. `body_base64` replaces `body` for bytes.
|
|
66
|
+
- **App messages** - `messages(listener)` hands the listener each message
|
|
67
|
+
another holder of this App sends to this User's running copies, as `from`,
|
|
68
|
+
the sender's User id, and `body`, the JSON value they sent, until the
|
|
69
|
+
function it returns is called. App code sends one through the Runtime's
|
|
70
|
+
`call('apps/messages/send', { to, body })` with the recipient's User id and
|
|
71
|
+
a body of at most 32 KiB of JSON; House fills in the App and relays it only
|
|
72
|
+
to copies running now, storing nothing. The panel sends none.
|
|
73
|
+
- **App screenshot read** - given an App reference, `readScreenshot(app)`
|
|
74
|
+
answers the image this browser captured of that App's screen the first time
|
|
75
|
+
it ran its current release, as a PNG `Blob` at most 1200 pixels wide, or
|
|
76
|
+
`null` when this browser holds none. The image stays in the browser; a Site
|
|
77
|
+
holds none.
|
|
78
|
+
- **Site screenshot read** - given one of the User's Site identifiers,
|
|
79
|
+
`readSiteScreenshot(site)` answers the image House captured of that Site
|
|
80
|
+
after its latest release, as a JPEG `Blob`, or `null` when House holds none
|
|
81
|
+
or does not answer. A Site's own renderer reads none.
|
|
82
|
+
|
|
83
|
+
## The test host
|
|
84
|
+
|
|
85
|
+
The host plays the shell and the worker: it delivers the address the App starts
|
|
86
|
+
at and each address `open` plays as Back or Forward through the shell's own
|
|
87
|
+
slice below the App's path, admitting nothing, so it delivers `//notes` exactly
|
|
88
|
+
as a browser at `/apps/<app>//notes` does. It records every push and replace
|
|
89
|
+
and answers each with the address the shell would then show, records an
|
|
90
|
+
admitted ceremony, refuses a push, replace or ceremony that is not a
|
|
91
|
+
same-origin absolute path, publishes a status, its `service_mode` included,
|
|
92
|
+
records in `consents` each
|
|
93
|
+
`gaConsent` it takes while that status measures and publishes the status the
|
|
94
|
+
choice leaves, records in `tabs` each `setTab` the shell would take and ignores
|
|
95
|
+
any other, answers a release code read from the `sources` it was given, or
|
|
96
|
+
`null`, answers each `callExternal` from the `external` function it was given, or
|
|
97
|
+
`null`, and records the request in `externalCalls`, answers `readScreenshot`
|
|
98
|
+
from the `screenshots` it was given by App
|
|
99
|
+
reference and `readSiteScreenshot` from the `siteScreenshots` it was given by
|
|
100
|
+
Site identifier, answers every `install()` `'manual'`, as a browser with no install
|
|
101
|
+
prompt does, answers every `notifications()` `'unavailable'`, as a browser
|
|
102
|
+
without push does, settles every `setBadge`, delivers each App message
|
|
103
|
+
`message({ from, body })` plays, and hands `bridge.runtime` the MD Runtime
|
|
104
|
+
client `attachRuntime(client)` gives it.
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentshouse/app-bridge",
|
|
3
|
-
"version": "0.1.0-
|
|
4
|
-
"description": "Renderer-side client of the agents.house App bridge: navigation, the tab title and icon, ceremonies, installation, status, Site image reads, App release code and
|
|
5
|
-
"license": "
|
|
3
|
+
"version": "0.1.0-c2b8788fb515c",
|
|
4
|
+
"description": "Renderer-side client of the agents.house App bridge: navigation, the tab title and icon, ceremonies, installation, notifications and the icon badge, status, Site image reads, App release code, App and Site screenshot reads, and App messages, with an in-page test host.",
|
|
5
|
+
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"engines": {
|
|
@@ -10,21 +10,21 @@
|
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
-
"types": "./client/src/index.d.ts",
|
|
14
|
-
"default": "./client/src/index.js"
|
|
13
|
+
"types": "./dist/client/src/index.d.ts",
|
|
14
|
+
"default": "./dist/client/src/index.js"
|
|
15
15
|
},
|
|
16
16
|
"./test-host": {
|
|
17
|
-
"types": "./client/src/test-host.d.ts",
|
|
18
|
-
"default": "./client/src/test-host.js"
|
|
17
|
+
"types": "./dist/client/src/test-host.d.ts",
|
|
18
|
+
"default": "./dist/client/src/test-host.js"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@agentshouse/mdruntime": "0.1.0-ca9a5a0b81b98"
|
|
23
|
+
},
|
|
21
24
|
"files": [
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"src/apps/tab.js",
|
|
27
|
-
"src/apps/tab.d.ts",
|
|
28
|
-
"README.md"
|
|
25
|
+
"dist",
|
|
26
|
+
"docs",
|
|
27
|
+
"README.md",
|
|
28
|
+
"LICENSE"
|
|
29
29
|
]
|
|
30
30
|
}
|
package/client/src/index.d.ts
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
export { connectHouseBridge, HouseCeremonyRefused, HouseNavigationRefused, type HouseBridge, type HouseImageCopy, } from './bridge.js';
|
|
2
|
-
export { HOUSE_ADDRESS_MAX_CHARS } from './wire.js';
|
|
3
|
-
export type { HouseBridgeEvent, HouseBridgePort, HouseCredentialState, HouseGa, HouseGaChoice, HouseMessagePort, HouseNavigationMode, HouseSourceDownload, HouseSourceFile, HouseSourceRefusal, HouseSourceTop, HouseStatus, HouseSyncState, } from './wire.js';
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|