@agentshouse/app-bridge 0.1.0-alpha.5 → 0.1.0-alpha.7
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 +21 -3
- package/client/src/bridge.d.ts +4 -1
- package/client/src/bridge.js +29 -0
- package/client/src/index.d.ts +1 -1
- package/client/src/test-host.d.ts +6 -1
- package/client/src/test-host.js +14 -0
- package/client/src/wire.d.ts +31 -0
- package/package.json +4 -2
- package/src/apps/tab.d.ts +6 -0
- package/src/apps/tab.js +27 -0
package/README.md
CHANGED
|
@@ -2,11 +2,16 @@
|
|
|
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 five families:
|
|
6
6
|
|
|
7
7
|
- **Navigation** - the part of the shell's URL below the App's path, with its
|
|
8
8
|
query and fragment. The shell delivers it when the App starts and whenever the
|
|
9
|
-
User navigates; the App asks the shell to push or replace it.
|
|
9
|
+
User navigates; the App asks the shell to push or replace it. The App also
|
|
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.
|
|
10
15
|
- **Ceremony** - the App asks the shell to leave for one of House's own
|
|
11
16
|
purpose-bound authenticated pages. The shell navigates only to a same-origin
|
|
12
17
|
absolute path, carries its current address as the return target, and the page
|
|
@@ -24,6 +29,16 @@ port's one listener. It carries four families:
|
|
|
24
29
|
- **Site image read** - given a Published Room and an original attachment
|
|
25
30
|
reference, it answers the web copy's path and metadata, or `null` when no
|
|
26
31
|
copy exists. The original bytes never cross this bridge at a Site.
|
|
32
|
+
- **App release code** - `sourceTop(app, release)` answers the top level of
|
|
33
|
+
the latest successful release of an App the User may use: its folders, its
|
|
34
|
+
files with their sizes and its root README's text, or `null` for none;
|
|
35
|
+
`sourceDownload(app, release)` answers every file of that release with its
|
|
36
|
+
path, its size and its `text`, or its bytes as `base64`. The browser keeps
|
|
37
|
+
each answer per App and release: a read naming a release it holds answers
|
|
38
|
+
without asking House, and keeping a newer release drops the earlier one. A
|
|
39
|
+
refusal is House's `{ error: { code } }`, `app_release_not_found` for an
|
|
40
|
+
App with no release; a read House does not answer, or one that outlived
|
|
41
|
+
the browser's signed-in User, is `null`.
|
|
27
42
|
|
|
28
43
|
The package carries no credential handling and no Runtime family. It surfaces
|
|
29
44
|
the dedicated Runtime port the worker transfers over the bridge as a plain
|
|
@@ -40,6 +55,7 @@ const md = connectMdRuntime(await bridge.runtime);
|
|
|
40
55
|
|
|
41
56
|
bridge.onAddress((address) => render(address));
|
|
42
57
|
bridge.onStatus((status) => showSync(status.sync));
|
|
58
|
+
bridge.setTab({ title: 'Atlas (3)' });
|
|
43
59
|
|
|
44
60
|
open('/rooms/atlas');
|
|
45
61
|
function open(address) {
|
|
@@ -81,4 +97,6 @@ admitted ceremony, refuses a push, replace or ceremony that is not a
|
|
|
81
97
|
same-origin absolute path, publishes a status, its `service_mode` included,
|
|
82
98
|
records in `consents` each
|
|
83
99
|
`gaConsent` it takes while that status measures and publishes the status the
|
|
84
|
-
choice leaves,
|
|
100
|
+
choice leaves, records in `tabs` each `setTab` the shell would take and ignores
|
|
101
|
+
any other, answers a release code read from the `sources` it was given, or
|
|
102
|
+
`null`, and attaches a Runtime port.
|
package/client/src/bridge.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HouseBridgePort, HouseGaChoice, HouseMessagePort, HouseStatus } from './wire.js';
|
|
1
|
+
import type { HouseBridgePort, HouseGaChoice, HouseMessagePort, HouseSourceDownload, HouseSourceRefusal, HouseSourceTop, HouseStatus, HouseTab } from './wire.js';
|
|
2
2
|
export declare class HouseCeremonyRefused extends Error {
|
|
3
3
|
readonly path: string | null;
|
|
4
4
|
constructor(path: string | null);
|
|
@@ -20,7 +20,10 @@ export interface HouseBridge {
|
|
|
20
20
|
push(address: string): Promise<string>;
|
|
21
21
|
replace(address: string): Promise<string>;
|
|
22
22
|
ceremony(path: string): Promise<never>;
|
|
23
|
+
setTab(tab: HouseTab): void;
|
|
23
24
|
readAttachment(room: string, attachment: string): Promise<HouseImageCopy | null>;
|
|
25
|
+
sourceTop(app: string, release?: string): Promise<HouseSourceTop | HouseSourceRefusal | null>;
|
|
26
|
+
sourceDownload(app: string, release?: string): Promise<HouseSourceDownload | HouseSourceRefusal | null>;
|
|
24
27
|
status(): HouseStatus | null;
|
|
25
28
|
onStatus(listener: (status: HouseStatus) => void): () => void;
|
|
26
29
|
gaConsent(choice: HouseGaChoice): void;
|
package/client/src/bridge.js
CHANGED
|
@@ -33,6 +33,8 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
33
33
|
const navigations = [];
|
|
34
34
|
const attachments = new Map();
|
|
35
35
|
let nextAttachment = 0;
|
|
36
|
+
const sources = new Map();
|
|
37
|
+
let nextSource = 0;
|
|
36
38
|
const addressListeners = new Set();
|
|
37
39
|
const statusListeners = new Set();
|
|
38
40
|
let outbox = [];
|
|
@@ -88,6 +90,17 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
88
90
|
pending.resolve(answer.image ?? null);
|
|
89
91
|
return;
|
|
90
92
|
}
|
|
93
|
+
if (data.house === 'source') {
|
|
94
|
+
const answer = data;
|
|
95
|
+
if (typeof answer.id !== 'number')
|
|
96
|
+
return;
|
|
97
|
+
const pending = sources.get(answer.id);
|
|
98
|
+
if (pending === undefined)
|
|
99
|
+
return;
|
|
100
|
+
sources.delete(answer.id);
|
|
101
|
+
pending.resolve(answer.answer ?? null);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
91
104
|
if (data.house === 'ceremony-refused') {
|
|
92
105
|
const refused = data.path;
|
|
93
106
|
const named = typeof refused === 'string' ? refused : null;
|
|
@@ -105,6 +118,13 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
105
118
|
for (const message of waiting)
|
|
106
119
|
port.postMessage(message);
|
|
107
120
|
});
|
|
121
|
+
function readSource(house, app, release) {
|
|
122
|
+
const pending = settled();
|
|
123
|
+
const id = ++nextSource;
|
|
124
|
+
sources.set(id, pending.settle);
|
|
125
|
+
send({ house, id, app, release });
|
|
126
|
+
return pending.promise;
|
|
127
|
+
}
|
|
108
128
|
function navigate(mode, asked) {
|
|
109
129
|
const pending = settled();
|
|
110
130
|
navigations.push({ address: asked, settle: pending.settle });
|
|
@@ -134,6 +154,9 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
134
154
|
send({ house: 'ceremony', path });
|
|
135
155
|
return pending.promise;
|
|
136
156
|
},
|
|
157
|
+
setTab(tab) {
|
|
158
|
+
send({ house: 'tab', title: tab.title, icon: tab.icon });
|
|
159
|
+
},
|
|
137
160
|
readAttachment(room, attachment) {
|
|
138
161
|
const pending = settled();
|
|
139
162
|
const id = ++nextAttachment;
|
|
@@ -141,6 +164,12 @@ export function connectHouseBridge(source = heldBridge()) {
|
|
|
141
164
|
send({ house: 'attachment-read', id, room, attachment });
|
|
142
165
|
return pending.promise;
|
|
143
166
|
},
|
|
167
|
+
sourceTop(app, release) {
|
|
168
|
+
return readSource('source-top', app, release);
|
|
169
|
+
},
|
|
170
|
+
sourceDownload(app, release) {
|
|
171
|
+
return readSource('source-download', app, release);
|
|
172
|
+
},
|
|
144
173
|
status() {
|
|
145
174
|
return status;
|
|
146
175
|
},
|
package/client/src/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { connectHouseBridge, HouseCeremonyRefused, HouseNavigationRefused, type HouseBridge, type HouseImageCopy, } from './bridge.js';
|
|
2
2
|
export { HOUSE_ADDRESS_MAX_CHARS } from './wire.js';
|
|
3
|
-
export type { HouseBridgeEvent, HouseBridgePort, HouseCredentialState, HouseGa, HouseGaChoice, HouseMessagePort, HouseNavigationMode, HouseStatus, HouseSyncState, } from './wire.js';
|
|
3
|
+
export type { HouseBridgeEvent, HouseBridgePort, HouseCredentialState, HouseGa, HouseGaChoice, HouseMessagePort, HouseNavigationMode, HouseSourceDownload, HouseSourceFile, HouseSourceRefusal, HouseSourceTop, HouseStatus, HouseSyncState, } from './wire.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HouseImageCopy } from './bridge.js';
|
|
2
|
-
import type { HouseBridgePort, HouseGaChoice, HouseMessagePort, HouseNavigationMode, HouseStatus } from './wire.js';
|
|
2
|
+
import type { HouseBridgePort, HouseGaChoice, HouseMessagePort, HouseNavigationMode, HouseSourceDownload, HouseSourceTop, HouseStatus, HouseTab } from './wire.js';
|
|
3
3
|
export interface HouseBridgeNavigation {
|
|
4
4
|
readonly mode: HouseNavigationMode;
|
|
5
5
|
readonly address: string;
|
|
@@ -9,6 +9,7 @@ export interface HouseBridgeTestHost {
|
|
|
9
9
|
readonly navigations: readonly HouseBridgeNavigation[];
|
|
10
10
|
readonly ceremonies: readonly string[];
|
|
11
11
|
readonly consents: readonly HouseGaChoice[];
|
|
12
|
+
readonly tabs: readonly HouseTab[];
|
|
12
13
|
address(): string;
|
|
13
14
|
open(address: string): void;
|
|
14
15
|
publish(status: HouseStatus): void;
|
|
@@ -20,5 +21,9 @@ export interface HouseBridgeTestHostOptions {
|
|
|
20
21
|
readonly images?: readonly (HouseImageCopy & {
|
|
21
22
|
readonly room: string;
|
|
22
23
|
})[];
|
|
24
|
+
readonly sources?: readonly {
|
|
25
|
+
readonly top: HouseSourceTop;
|
|
26
|
+
readonly download: HouseSourceDownload;
|
|
27
|
+
}[];
|
|
23
28
|
}
|
|
24
29
|
export declare function createHouseBridgeTestHost(options?: HouseBridgeTestHostOptions): HouseBridgeTestHost;
|
package/client/src/test-host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { appAddressOf } from '../../src/apps/remainder.js';
|
|
2
|
+
import { tabChange } from '../../src/apps/tab.js';
|
|
2
3
|
import { HOUSE_ADDRESS_MAX_CHARS } from './wire.js';
|
|
3
4
|
const HOST_ORIGIN = 'https://house.invalid';
|
|
4
5
|
function sameOriginPath(value) {
|
|
@@ -15,6 +16,7 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
15
16
|
const navigations = [];
|
|
16
17
|
const ceremonies = [];
|
|
17
18
|
const consents = [];
|
|
19
|
+
const tabs = [];
|
|
18
20
|
const listeners = new Set();
|
|
19
21
|
const queued = [];
|
|
20
22
|
let started = false;
|
|
@@ -77,9 +79,20 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
77
79
|
else if (data.house === 'ceremony') {
|
|
78
80
|
ceremony(data.path);
|
|
79
81
|
}
|
|
82
|
+
else if (data.house === 'tab') {
|
|
83
|
+
const change = tabChange(data);
|
|
84
|
+
if (change !== null)
|
|
85
|
+
tabs.push(change);
|
|
86
|
+
}
|
|
80
87
|
else if (data.house === 'ga/consent') {
|
|
81
88
|
consent(data.choice);
|
|
82
89
|
}
|
|
90
|
+
else if (data.house === 'source-top' || data.house === 'source-download') {
|
|
91
|
+
const asked = data;
|
|
92
|
+
const source = options.sources?.find((entry) => entry.top.app_ref === asked.app);
|
|
93
|
+
const answer = source === undefined ? null : data.house === 'source-top' ? source.top : source.download;
|
|
94
|
+
deliver({ house: 'source', id: asked.id, answer });
|
|
95
|
+
}
|
|
83
96
|
else if (data.house === 'attachment-read') {
|
|
84
97
|
const asked = data;
|
|
85
98
|
const image = options.images?.find((entry) => entry.room === asked.room && entry.attachment === asked.attachment);
|
|
@@ -123,6 +136,7 @@ export function createHouseBridgeTestHost(options = {}) {
|
|
|
123
136
|
navigations,
|
|
124
137
|
ceremonies,
|
|
125
138
|
consents,
|
|
139
|
+
tabs,
|
|
126
140
|
address() {
|
|
127
141
|
return address;
|
|
128
142
|
},
|
package/client/src/wire.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TabChange } from '../../src/apps/tab.js';
|
|
1
2
|
export interface HouseBridgeEvent {
|
|
2
3
|
readonly data: unknown;
|
|
3
4
|
readonly ports: readonly HouseMessagePort[];
|
|
@@ -35,4 +36,34 @@ export interface HouseStatus {
|
|
|
35
36
|
readonly service_mode: 'open' | 'write-halted' | null;
|
|
36
37
|
}
|
|
37
38
|
export type HouseNavigationMode = 'push' | 'replace';
|
|
39
|
+
interface HouseRelease {
|
|
40
|
+
readonly app_ref: string;
|
|
41
|
+
readonly release: string;
|
|
42
|
+
}
|
|
43
|
+
export interface HouseSourceTop extends HouseRelease {
|
|
44
|
+
readonly folders: readonly string[];
|
|
45
|
+
readonly files: readonly {
|
|
46
|
+
readonly name: string;
|
|
47
|
+
readonly size: number;
|
|
48
|
+
}[];
|
|
49
|
+
readonly readme: string | null;
|
|
50
|
+
}
|
|
51
|
+
export type HouseSourceFile = {
|
|
52
|
+
readonly path: string;
|
|
53
|
+
readonly size: number;
|
|
54
|
+
} & ({
|
|
55
|
+
readonly text: string;
|
|
56
|
+
} | {
|
|
57
|
+
readonly base64: string;
|
|
58
|
+
});
|
|
59
|
+
export interface HouseSourceDownload extends HouseRelease {
|
|
60
|
+
readonly files: readonly HouseSourceFile[];
|
|
61
|
+
}
|
|
62
|
+
export interface HouseSourceRefusal {
|
|
63
|
+
readonly error: {
|
|
64
|
+
readonly code: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export type HouseTab = TabChange;
|
|
38
68
|
export declare const HOUSE_ADDRESS_MAX_CHARS = 1000;
|
|
69
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentshouse/app-bridge",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "Renderer-side client of the agents.house App bridge: navigation, ceremonies, status
|
|
3
|
+
"version": "0.1.0-alpha.7",
|
|
4
|
+
"description": "Renderer-side client of the agents.house App bridge: navigation, the tab title and icon, ceremonies, status, Site image reads and App release code, with an in-page test host.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"client/src/*.d.ts",
|
|
24
24
|
"src/apps/remainder.js",
|
|
25
25
|
"src/apps/remainder.d.ts",
|
|
26
|
+
"src/apps/tab.js",
|
|
27
|
+
"src/apps/tab.d.ts",
|
|
26
28
|
"README.md"
|
|
27
29
|
]
|
|
28
30
|
}
|
package/src/apps/tab.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const TAB_TITLE_MAX_CHARS = 200;
|
|
2
|
+
const TAB_ICON_MAX_CHARS = 1_048_576;
|
|
3
|
+
function caseless(text) {
|
|
4
|
+
return text.replace(/[a-z]/gu, (letter) => `[${letter}${letter.toUpperCase()}]`);
|
|
5
|
+
}
|
|
6
|
+
export const TAB_IMAGE = new RegExp(`^${caseless('data:image/')}(?:svg\\+xml|${['png', 'gif', 'webp'].map(caseless).join('|')});${caseless('base64')},[A-Za-z0-9+/]+={0,2}$`, 'u');
|
|
7
|
+
function admittedTitle(title) {
|
|
8
|
+
return typeof title === 'string' && title.length <= TAB_TITLE_MAX_CHARS;
|
|
9
|
+
}
|
|
10
|
+
function admittedIcon(icon) {
|
|
11
|
+
return typeof icon === 'string' && icon.length <= TAB_ICON_MAX_CHARS && TAB_IMAGE.test(icon);
|
|
12
|
+
}
|
|
13
|
+
export function tabChange(asked) {
|
|
14
|
+
if (asked === null || typeof asked !== 'object')
|
|
15
|
+
return null;
|
|
16
|
+
const { title, icon } = asked;
|
|
17
|
+
if (title === undefined && icon === undefined)
|
|
18
|
+
return null;
|
|
19
|
+
if (title !== undefined && !admittedTitle(title))
|
|
20
|
+
return null;
|
|
21
|
+
if (icon !== undefined && !admittedIcon(icon))
|
|
22
|
+
return null;
|
|
23
|
+
return {
|
|
24
|
+
...(title === undefined ? {} : { title: title }),
|
|
25
|
+
...(icon === undefined ? {} : { icon: icon }),
|
|
26
|
+
};
|
|
27
|
+
}
|