@agentshouse/app-bridge 0.1.0-c3a0f0faaf74b → 0.1.0-c58a055eeaa71
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 +14 -11
- package/dist/index.d.ts +139 -0
- package/dist/index.js +1 -0
- package/dist/{client/src/test-host.d.ts → test-host.d.ts} +3 -4
- package/dist/test-host.js +165 -0
- package/docs/families.md +37 -34
- package/package.json +7 -7
- package/dist/client/src/bridge.d.ts +0 -46
- package/dist/client/src/bridge.js +0 -293
- package/dist/client/src/index.d.ts +0 -3
- package/dist/client/src/index.js +0 -2
- package/dist/client/src/test-host.js +0 -185
- package/dist/client/src/wire.d.ts +0 -87
- package/dist/client/src/wire.js +0 -1
- package/dist/src/apps/remainder.d.ts +0 -2
- package/dist/src/apps/remainder.js +0 -16
- package/dist/src/apps/tab.d.ts +0 -6
- package/dist/src/apps/tab.js +0 -27
package/README.md
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
1
|
# @agentshouse/app-bridge
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
The types of the agents.house App bridge, an accessor and an in-page test host.
|
|
4
|
+
House builds the bridge object in every App it serves and hands it to the page
|
|
5
|
+
before the App's own scripts run; `houseBridge()` returns that object, so an App
|
|
6
|
+
carries no bridge code of its own. It carries eleven families: navigation,
|
|
7
|
+
ceremonies, installation, notifications, status, Site image reads, App release
|
|
8
|
+
code, external calls, App messages and App and Site screenshot reads.
|
|
8
9
|
[docs/families.md](docs/families.md) describes each and the test host.
|
|
9
10
|
|
|
10
|
-
The
|
|
11
|
+
The bridge carries no credential handling and no Runtime family. House builds
|
|
11
12
|
the MD Runtime client into the page beside the bridge, and `bridge.runtime`
|
|
12
13
|
resolves to that client, so the App imports and constructs none.
|
|
13
14
|
|
|
15
|
+
House's `find_command` "App" is the guide to building an App or a Site.
|
|
16
|
+
|
|
14
17
|
## Use
|
|
15
18
|
|
|
16
19
|
```js
|
|
17
|
-
import {
|
|
20
|
+
import { houseBridge } from '@agentshouse/app-bridge';
|
|
18
21
|
|
|
19
|
-
const bridge =
|
|
22
|
+
const bridge = houseBridge();
|
|
20
23
|
const md = await bridge.runtime;
|
|
21
24
|
|
|
22
25
|
bridge.onAddress((address) => render(address));
|
|
@@ -32,7 +35,7 @@ async function manageSecurity(securityPath) {
|
|
|
32
35
|
try {
|
|
33
36
|
await bridge.ceremony(securityPath);
|
|
34
37
|
} catch (refusal) {
|
|
35
|
-
report(refusal);
|
|
38
|
+
if (refusal.name === 'HouseCeremonyRefused') report(refusal.path);
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
```
|
|
@@ -43,11 +46,11 @@ async function manageSecurity(securityPath) {
|
|
|
43
46
|
## Test without a House
|
|
44
47
|
|
|
45
48
|
```js
|
|
46
|
-
import {
|
|
49
|
+
import { houseBridge } from '@agentshouse/app-bridge';
|
|
47
50
|
import { createHouseBridgeTestHost } from '@agentshouse/app-bridge/test-host';
|
|
48
51
|
|
|
49
52
|
const host = createHouseBridgeTestHost({ address: '/rooms/atlas' });
|
|
50
|
-
const bridge =
|
|
53
|
+
const bridge = houseBridge();
|
|
51
54
|
|
|
52
55
|
bridge.push('/rooms/atlas/members');
|
|
53
56
|
host.open('/rooms/atlas');
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { MdRuntimeClient } from '@agentshouse/mdruntime/client';
|
|
2
|
+
type Open<Value extends string> = Value | (string & {});
|
|
3
|
+
export interface HouseSyncState {
|
|
4
|
+
readonly state: string;
|
|
5
|
+
readonly providerUnreachable: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface HouseCredentialState {
|
|
8
|
+
readonly state: Open<'none' | 'held' | 'rejected'>;
|
|
9
|
+
readonly expiresAt: number | null;
|
|
10
|
+
}
|
|
11
|
+
export type HouseAnalyticsChoice = 'accept' | 'decline';
|
|
12
|
+
export interface HouseAnalytics {
|
|
13
|
+
readonly measures: boolean;
|
|
14
|
+
readonly choice: Open<HouseAnalyticsChoice> | null;
|
|
15
|
+
}
|
|
16
|
+
export interface HouseStatus {
|
|
17
|
+
readonly app: string | null;
|
|
18
|
+
readonly preferences: {
|
|
19
|
+
readonly roomRef: string;
|
|
20
|
+
readonly path: string;
|
|
21
|
+
} | null;
|
|
22
|
+
readonly runtime: string;
|
|
23
|
+
readonly credential: HouseCredentialState;
|
|
24
|
+
readonly sync: HouseSyncState | null;
|
|
25
|
+
readonly refusal: {
|
|
26
|
+
readonly code: string;
|
|
27
|
+
readonly retryable: boolean;
|
|
28
|
+
} | null;
|
|
29
|
+
readonly loss: string | null;
|
|
30
|
+
readonly analytics: HouseAnalytics;
|
|
31
|
+
readonly serviceMode: Open<'open' | 'write-halted'> | null;
|
|
32
|
+
readonly offline: boolean;
|
|
33
|
+
}
|
|
34
|
+
export type HouseNavigationMode = 'push' | 'replace';
|
|
35
|
+
export interface HouseTab {
|
|
36
|
+
readonly title?: string;
|
|
37
|
+
readonly icon?: string;
|
|
38
|
+
}
|
|
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 HouseExternalRequest {
|
|
63
|
+
readonly url: string;
|
|
64
|
+
readonly method?: string;
|
|
65
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
66
|
+
readonly body?: string;
|
|
67
|
+
readonly body_base64?: string;
|
|
68
|
+
readonly account?: string;
|
|
69
|
+
readonly slot?: string;
|
|
70
|
+
}
|
|
71
|
+
export type HouseExternalAnswer = {
|
|
72
|
+
readonly status: number;
|
|
73
|
+
readonly headers: Readonly<Record<string, string | readonly string[]>>;
|
|
74
|
+
} & ({
|
|
75
|
+
readonly body: string;
|
|
76
|
+
} | {
|
|
77
|
+
readonly body_base64: string;
|
|
78
|
+
});
|
|
79
|
+
export interface HouseRefusal {
|
|
80
|
+
readonly error: {
|
|
81
|
+
readonly code: string;
|
|
82
|
+
readonly retryable: boolean;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export interface HouseAppMessage {
|
|
86
|
+
readonly from: string;
|
|
87
|
+
readonly body: unknown;
|
|
88
|
+
}
|
|
89
|
+
export type HouseInstallation = Open<'accepted' | 'dismissed' | 'manual'>;
|
|
90
|
+
export type HouseNotifications = Open<'on' | 'off' | 'blocked' | 'unavailable'>;
|
|
91
|
+
export interface HouseImageCopy {
|
|
92
|
+
readonly attachment: string;
|
|
93
|
+
readonly copy: string;
|
|
94
|
+
readonly name: string;
|
|
95
|
+
readonly media_type: string;
|
|
96
|
+
}
|
|
97
|
+
export interface HouseCeremonyRefused extends Error {
|
|
98
|
+
readonly name: 'HouseCeremonyRefused';
|
|
99
|
+
readonly code: Open<'house_ceremony_refused'>;
|
|
100
|
+
readonly retryable: boolean;
|
|
101
|
+
readonly path: string | null;
|
|
102
|
+
}
|
|
103
|
+
export interface HouseNavigationRefused extends Error {
|
|
104
|
+
readonly name: 'HouseNavigationRefused';
|
|
105
|
+
readonly code: Open<'house_navigation_refused'>;
|
|
106
|
+
readonly retryable: boolean;
|
|
107
|
+
readonly address: string | null;
|
|
108
|
+
}
|
|
109
|
+
export interface HouseBrowserRefused extends Error {
|
|
110
|
+
readonly code: string;
|
|
111
|
+
readonly retryable: boolean;
|
|
112
|
+
}
|
|
113
|
+
export interface HouseBridge {
|
|
114
|
+
readonly runtime: Promise<MdRuntimeClient>;
|
|
115
|
+
address(): string | null;
|
|
116
|
+
onAddress(listener: (address: string) => void): () => void;
|
|
117
|
+
push(address: string): Promise<string>;
|
|
118
|
+
replace(address: string): Promise<string>;
|
|
119
|
+
ceremony(path: string): Promise<never>;
|
|
120
|
+
install(): Promise<HouseInstallation>;
|
|
121
|
+
notifications(on?: boolean): Promise<HouseNotifications>;
|
|
122
|
+
setBadge(unread: readonly string[]): Promise<void>;
|
|
123
|
+
setTab(tab: HouseTab): void;
|
|
124
|
+
readAttachment(room: string, attachment: string): Promise<HouseImageCopy | null>;
|
|
125
|
+
sourceTop(app: string, release?: string): Promise<HouseSourceTop | HouseRefusal | null>;
|
|
126
|
+
sourceDownload(app: string, release?: string): Promise<HouseSourceDownload | HouseRefusal | null>;
|
|
127
|
+
callExternal(request: HouseExternalRequest): Promise<HouseExternalAnswer | HouseRefusal | null>;
|
|
128
|
+
readScreenshot(app: string): Promise<Blob | null>;
|
|
129
|
+
readSiteScreenshot(site: string): Promise<Blob | null>;
|
|
130
|
+
status(): HouseStatus | null;
|
|
131
|
+
onStatus(listener: (status: HouseStatus) => void): () => void;
|
|
132
|
+
messages(listener: (message: HouseAppMessage) => void): () => void;
|
|
133
|
+
analyticsConsent(choice: HouseAnalyticsChoice): void;
|
|
134
|
+
}
|
|
135
|
+
export interface HouseHeld {
|
|
136
|
+
readonly houseBridge: HouseBridge;
|
|
137
|
+
}
|
|
138
|
+
export declare const houseBridge: () => HouseBridge;
|
|
139
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const houseBridge = () => globalThis.houseBridge;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import type { MdRuntimeClient } from '@agentshouse/mdruntime/client';
|
|
2
|
-
import type {
|
|
3
|
-
import type { HouseAppMessage, HouseExternalAnswer, HouseExternalRequest, HouseGaChoice, HouseNavigationMode, HouseRefusal, HouseSourceDownload, HouseSourceTop, HouseStatus, HouseTab } from './wire.js';
|
|
2
|
+
import type { HouseAnalyticsChoice, HouseAppMessage, HouseExternalAnswer, HouseExternalRequest, HouseImageCopy, HouseNavigationMode, HouseRefusal, HouseSourceDownload, HouseSourceTop, HouseStatus, HouseTab } from './index.js';
|
|
4
3
|
export interface HouseBridgeNavigation {
|
|
5
4
|
readonly mode: HouseNavigationMode;
|
|
6
5
|
readonly address: string;
|
|
7
6
|
}
|
|
8
|
-
export interface HouseBridgeTestHost
|
|
7
|
+
export interface HouseBridgeTestHost {
|
|
9
8
|
readonly navigations: readonly HouseBridgeNavigation[];
|
|
10
9
|
readonly ceremonies: readonly string[];
|
|
11
|
-
readonly consents: readonly
|
|
10
|
+
readonly consents: readonly HouseAnalyticsChoice[];
|
|
12
11
|
readonly tabs: readonly HouseTab[];
|
|
13
12
|
readonly externalCalls: readonly HouseExternalRequest[];
|
|
14
13
|
address(): string;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
const HOST_ORIGIN = 'https://house.invalid';
|
|
2
|
+
const ADDRESS_MAX_CHARS = 1000;
|
|
3
|
+
function sameOriginPath(value) {
|
|
4
|
+
if (typeof value !== 'string')
|
|
5
|
+
return null;
|
|
6
|
+
if (value.charAt(0) !== '/' || value.slice(0, 2) === '//')
|
|
7
|
+
return null;
|
|
8
|
+
if (value.length > ADDRESS_MAX_CHARS)
|
|
9
|
+
return null;
|
|
10
|
+
const target = new URL(value, HOST_ORIGIN);
|
|
11
|
+
return target.origin === HOST_ORIGIN ? `${target.pathname}${target.search}${target.hash}` : null;
|
|
12
|
+
}
|
|
13
|
+
function appAddress(address) {
|
|
14
|
+
return address.startsWith('/') ? address : `/${address}`;
|
|
15
|
+
}
|
|
16
|
+
function ceremonyRefused(path) {
|
|
17
|
+
return Object.assign(new Error('house_ceremony_refused'), {
|
|
18
|
+
name: 'HouseCeremonyRefused',
|
|
19
|
+
code: 'house_ceremony_refused',
|
|
20
|
+
retryable: false,
|
|
21
|
+
path,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function navigationRefused(address) {
|
|
25
|
+
return Object.assign(new Error('house_navigation_refused'), {
|
|
26
|
+
name: 'HouseNavigationRefused',
|
|
27
|
+
code: 'house_navigation_refused',
|
|
28
|
+
retryable: false,
|
|
29
|
+
address,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
export function createHouseBridgeTestHost(options = {}) {
|
|
33
|
+
const navigations = [];
|
|
34
|
+
const ceremonies = [];
|
|
35
|
+
const consents = [];
|
|
36
|
+
const tabs = [];
|
|
37
|
+
const externalCalls = [];
|
|
38
|
+
const addressListeners = new Set();
|
|
39
|
+
const statusListeners = new Set();
|
|
40
|
+
const messageListeners = new Set();
|
|
41
|
+
let current = appAddress(options.address ?? '/');
|
|
42
|
+
let published = options.status ?? null;
|
|
43
|
+
let shown = null;
|
|
44
|
+
let seen = null;
|
|
45
|
+
let attached = () => { };
|
|
46
|
+
const runtime = new Promise((resolve) => {
|
|
47
|
+
attached = resolve;
|
|
48
|
+
});
|
|
49
|
+
function arrive(target) {
|
|
50
|
+
current = target;
|
|
51
|
+
queueMicrotask(() => {
|
|
52
|
+
shown = target;
|
|
53
|
+
for (const listener of addressListeners)
|
|
54
|
+
listener(target);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function publish(next) {
|
|
58
|
+
published = next;
|
|
59
|
+
queueMicrotask(() => {
|
|
60
|
+
seen = next;
|
|
61
|
+
for (const listener of statusListeners)
|
|
62
|
+
listener(next);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function navigate(mode, asked) {
|
|
66
|
+
const target = sameOriginPath(asked);
|
|
67
|
+
if (target === null)
|
|
68
|
+
return Promise.reject(navigationRefused(asked));
|
|
69
|
+
navigations.push({ mode, address: target });
|
|
70
|
+
arrive(target);
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
queueMicrotask(() => resolve(target));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
const bridge = {
|
|
76
|
+
runtime,
|
|
77
|
+
address: () => shown,
|
|
78
|
+
onAddress(listener) {
|
|
79
|
+
addressListeners.add(listener);
|
|
80
|
+
return () => {
|
|
81
|
+
addressListeners.delete(listener);
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
push: (asked) => navigate('push', asked),
|
|
85
|
+
replace: (asked) => navigate('replace', asked),
|
|
86
|
+
ceremony(path) {
|
|
87
|
+
const target = sameOriginPath(path);
|
|
88
|
+
if (target === null)
|
|
89
|
+
return Promise.reject(ceremonyRefused(path));
|
|
90
|
+
ceremonies.push(target);
|
|
91
|
+
return new Promise(() => { });
|
|
92
|
+
},
|
|
93
|
+
install: () => Promise.resolve('manual'),
|
|
94
|
+
notifications: () => Promise.resolve('unavailable'),
|
|
95
|
+
setBadge: () => Promise.resolve(),
|
|
96
|
+
setTab(tab) {
|
|
97
|
+
tabs.push(tab);
|
|
98
|
+
},
|
|
99
|
+
readAttachment(room, attachment) {
|
|
100
|
+
const image = options.images?.find((entry) => entry.room === room && entry.attachment === attachment);
|
|
101
|
+
return Promise.resolve(image === undefined
|
|
102
|
+
? null
|
|
103
|
+
: {
|
|
104
|
+
attachment: image.attachment,
|
|
105
|
+
copy: image.copy,
|
|
106
|
+
name: image.name,
|
|
107
|
+
media_type: image.media_type,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
sourceTop: (app) => Promise.resolve(options.sources?.find((entry) => entry.top.app_ref === app)?.top ?? null),
|
|
111
|
+
sourceDownload: (app) => Promise.resolve(options.sources?.find((entry) => entry.top.app_ref === app)?.download ?? null),
|
|
112
|
+
callExternal(request) {
|
|
113
|
+
externalCalls.push(request);
|
|
114
|
+
return Promise.resolve(options.external?.(request) ?? null);
|
|
115
|
+
},
|
|
116
|
+
readScreenshot: (app) => Promise.resolve(options.screenshots?.[app] ?? null),
|
|
117
|
+
readSiteScreenshot: (site) => Promise.resolve(options.siteScreenshots?.[site] ?? null),
|
|
118
|
+
status: () => seen,
|
|
119
|
+
onStatus(listener) {
|
|
120
|
+
statusListeners.add(listener);
|
|
121
|
+
return () => {
|
|
122
|
+
statusListeners.delete(listener);
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
messages(listener) {
|
|
126
|
+
messageListeners.add(listener);
|
|
127
|
+
return () => {
|
|
128
|
+
messageListeners.delete(listener);
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
analyticsConsent(choice) {
|
|
132
|
+
if (published === null || !published.analytics.measures)
|
|
133
|
+
return;
|
|
134
|
+
if (choice !== 'accept' && choice !== 'decline')
|
|
135
|
+
return;
|
|
136
|
+
consents.push(choice);
|
|
137
|
+
publish({ ...published, analytics: { measures: true, choice } });
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
Object.defineProperty(globalThis, 'houseBridge', { value: bridge, configurable: true });
|
|
141
|
+
if (published !== null)
|
|
142
|
+
publish(published);
|
|
143
|
+
arrive(current);
|
|
144
|
+
return {
|
|
145
|
+
navigations,
|
|
146
|
+
ceremonies,
|
|
147
|
+
consents,
|
|
148
|
+
tabs,
|
|
149
|
+
externalCalls,
|
|
150
|
+
address: () => current,
|
|
151
|
+
open(asked) {
|
|
152
|
+
arrive(appAddress(asked));
|
|
153
|
+
},
|
|
154
|
+
publish,
|
|
155
|
+
message({ from, body }) {
|
|
156
|
+
queueMicrotask(() => {
|
|
157
|
+
for (const listener of messageListeners)
|
|
158
|
+
listener({ from, body });
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
attachRuntime(client) {
|
|
162
|
+
attached(client);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
package/docs/families.md
CHANGED
|
@@ -18,28 +18,30 @@
|
|
|
18
18
|
`'accepted'` or `'dismissed'` with the User's choice, spending that prompt;
|
|
19
19
|
otherwise, as on every iPhone and iPad, it resolves `'manual'` and the App
|
|
20
20
|
shows its own instructions. A prompt the browser refuses to show, as for a
|
|
21
|
-
call made without a click, rejects with
|
|
22
|
-
and stays ready. Call it from a click. The standard
|
|
21
|
+
call made without a click, rejects with an error named and coded by the
|
|
22
|
+
browser's own error name, with `retryable` false, and stays ready. Call it from a click. The standard
|
|
23
23
|
`display-mode: standalone` media query tells the App it runs installed.
|
|
24
24
|
- **Notifications** - House's own panel alone; an App's or a Site's call
|
|
25
|
-
rejects with `HouseCeremonyRefused`. `notifications()` answers this browser
|
|
25
|
+
rejects with an error named `HouseCeremonyRefused`, carrying `code` and `retryable`. `notifications()` answers this browser
|
|
26
26
|
storage's push state: `'on'`, `'off'`, `'blocked'` where the browser refuses
|
|
27
27
|
notifications, or `'unavailable'` where it has no push, as on an iPhone or
|
|
28
28
|
iPad outside a Home-screen app. `notifications(true)` asks the browser's
|
|
29
29
|
permission and subscribes, `notifications(false)` unsubscribes, and each
|
|
30
|
-
answers the state it leaves; a failure of the browser's push service
|
|
31
|
-
|
|
30
|
+
answers the state it leaves; a failure of the browser's push service, and
|
|
31
|
+
of `setBadge`, rejects the same way. Call `notifications(true)` from a
|
|
32
32
|
click. `setBadge(unread)` takes the identifiers of the Human Notifications
|
|
33
33
|
unread in this storage and sets the icon's badge to their number, or clears
|
|
34
34
|
it at none, where the browser shows one.
|
|
35
|
-
- **Status** - the App reference the runtime client is attached under,
|
|
36
|
-
|
|
35
|
+
- **Status** - the App reference the runtime client is attached under,
|
|
36
|
+
`preferences`: the `roomRef` and Path of the App's own preferences document,
|
|
37
|
+
or `null` until the runtime holds that Room, sync
|
|
38
|
+
state, credential state and its `expiresAt`, observed failure, `analytics`:
|
|
37
39
|
whether the page measures and the visitor's stored analytics choice, and
|
|
38
|
-
`
|
|
40
|
+
`serviceMode`: `open`, `write-halted` while House refuses writes, so the
|
|
39
41
|
App can stop retrying them, or `null` while the runtime holds no House
|
|
40
|
-
connection.
|
|
41
|
-
The App reports the visitor's choice with `
|
|
42
|
-
`
|
|
42
|
+
connection. Every status field is camelCase.
|
|
43
|
+
The App reports the visitor's choice with `analyticsConsent('accept')` or
|
|
44
|
+
`analyticsConsent('decline')`; the shell hands it to House's analytics loader, and
|
|
43
45
|
the choice never reaches House.
|
|
44
46
|
- **Site image read** - given a Published Room and an original attachment
|
|
45
47
|
reference, it answers the web copy's path and metadata, or `null` when no
|
|
@@ -51,14 +53,14 @@
|
|
|
51
53
|
path, its size and its `text`, or its bytes as `base64`. The browser keeps
|
|
52
54
|
each answer per App and release: a read naming a release it holds answers
|
|
53
55
|
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
|
|
56
|
+
refusal is House's `{ error: { code, retryable } }`, `app_release_not_found` for an
|
|
55
57
|
App with no release; a read House does not answer, or one that outlived
|
|
56
58
|
the browser's signed-in User, is `null`.
|
|
57
59
|
- **External calls** - `callExternal({ url, method, headers, body, slot })`
|
|
58
60
|
has House's server send an HTTPS request to one of the release's declared
|
|
59
61
|
external origins and resolves with the service's `status`, `headers` and its
|
|
60
62
|
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
|
|
63
|
+
`{ error: { code, retryable } }`, or `null` when House does not answer. House fills in the
|
|
62
64
|
App and its running release; naming an account slot places the key of the
|
|
63
65
|
account the User connected to it, which the App never sees, and naming none
|
|
64
66
|
sends no key. A Site's call names no slot, and the House panel names an
|
|
@@ -67,7 +69,7 @@
|
|
|
67
69
|
another holder of this App sends to this User's running copies, as `from`,
|
|
68
70
|
the sender's User id, and `body`, the JSON value they sent, until the
|
|
69
71
|
function it returns is called. App code sends one through the Runtime's
|
|
70
|
-
`call('
|
|
72
|
+
`call('message.send', { to, body })` with the recipient's User id and
|
|
71
73
|
a body of at most 32 KiB of JSON; House fills in the App and relays it only
|
|
72
74
|
to copies running now, storing nothing. The panel sends none.
|
|
73
75
|
- **App screenshot read** - given an App reference, `readScreenshot(app)`
|
|
@@ -82,23 +84,24 @@
|
|
|
82
84
|
|
|
83
85
|
## The test host
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
`
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
`
|
|
97
|
-
`
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
`createHouseBridgeTestHost(options)` plays House in one page: it installs a
|
|
88
|
+
bridge object that `houseBridge()` returns, until the next host replaces it.
|
|
89
|
+
It delivers the address the App starts at and each address `open` plays as
|
|
90
|
+
Back or Forward, admitting nothing, so it delivers `//notes` exactly as a
|
|
91
|
+
browser at `/apps/<app>//notes` does. It records every push and replace and
|
|
92
|
+
answers each with the address the shell would then show, records an admitted
|
|
93
|
+
ceremony, and refuses a push, replace or ceremony that is not a same-origin
|
|
94
|
+
absolute path of at most 1,000 characters with an error named
|
|
95
|
+
`HouseNavigationRefused` or `HouseCeremonyRefused`. It publishes a status,
|
|
96
|
+
its `serviceMode` included, records in `consents` each `analyticsConsent` it
|
|
97
|
+
takes while that status measures and publishes the status the choice leaves,
|
|
98
|
+
records in `tabs` each `setTab`, answers a release code read from the
|
|
99
|
+
`sources` it was given, or `null`, answers each `callExternal` from the
|
|
100
|
+
`external` function it was given, or `null`, and records the request in
|
|
101
|
+
`externalCalls`, answers `readScreenshot` from the `screenshots` it was given
|
|
102
|
+
by App reference and `readSiteScreenshot` from the `siteScreenshots` it was
|
|
103
|
+
given by Site identifier, answers every `install()` `'manual'`, as a browser
|
|
104
|
+
with no install prompt does, answers every `notifications()` `'unavailable'`,
|
|
105
|
+
as a browser without push does, settles every `setBadge`, delivers each App
|
|
106
|
+
message `message({ from, body })` plays, and hands `bridge.runtime` the MD
|
|
107
|
+
Runtime client `attachRuntime(client)` gives it.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentshouse/app-bridge",
|
|
3
|
-
"version": "0.1.0-
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0-c58a055eeaa71",
|
|
4
|
+
"description": "Types of the agents.house App bridge House hands every App: 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 accessor and an in-page test host.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -10,16 +10,16 @@
|
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
-
"types": "./dist/
|
|
14
|
-
"default": "./dist/
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
15
|
},
|
|
16
16
|
"./test-host": {
|
|
17
|
-
"types": "./dist/
|
|
18
|
-
"default": "./dist/
|
|
17
|
+
"types": "./dist/test-host.d.ts",
|
|
18
|
+
"default": "./dist/test-host.js"
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@agentshouse/mdruntime": "0.1.0-
|
|
22
|
+
"@agentshouse/mdruntime": "0.1.0-c967d1ca66fee"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist",
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { MdRuntimeClient } from '@agentshouse/mdruntime/client';
|
|
2
|
-
import type { HouseAppMessage, HouseExternalAnswer, HouseExternalRequest, HouseBridgePort, HouseGaChoice, HouseRefusal, HouseSourceDownload, HouseSourceTop, HouseStatus, HouseTab } from './wire.js';
|
|
3
|
-
export declare class HouseCeremonyRefused extends Error {
|
|
4
|
-
readonly path: string | null;
|
|
5
|
-
constructor(path: string | null);
|
|
6
|
-
}
|
|
7
|
-
export declare class HouseNavigationRefused extends Error {
|
|
8
|
-
readonly address: string | null;
|
|
9
|
-
constructor(address: string | null);
|
|
10
|
-
}
|
|
11
|
-
export interface HouseImageCopy {
|
|
12
|
-
readonly attachment: string;
|
|
13
|
-
readonly copy: string;
|
|
14
|
-
readonly name: string;
|
|
15
|
-
readonly media_type: string;
|
|
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
|
-
}
|
|
23
|
-
export interface HouseBridge {
|
|
24
|
-
readonly runtime: Promise<MdRuntimeClient>;
|
|
25
|
-
address(): string | null;
|
|
26
|
-
onAddress(listener: (address: string) => void): () => void;
|
|
27
|
-
push(address: string): Promise<string>;
|
|
28
|
-
replace(address: string): Promise<string>;
|
|
29
|
-
ceremony(path: string): Promise<never>;
|
|
30
|
-
install(): Promise<HouseInstallation>;
|
|
31
|
-
notifications(on?: boolean): Promise<HouseNotifications>;
|
|
32
|
-
setBadge(unread: readonly string[]): Promise<void>;
|
|
33
|
-
setTab(tab: HouseTab): void;
|
|
34
|
-
readAttachment(room: string, attachment: string): Promise<HouseImageCopy | null>;
|
|
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>;
|
|
38
|
-
readScreenshot(app: string): Promise<Blob | null>;
|
|
39
|
-
readSiteScreenshot(site: string): Promise<Blob | null>;
|
|
40
|
-
status(): HouseStatus | null;
|
|
41
|
-
onStatus(listener: (status: HouseStatus) => void): () => void;
|
|
42
|
-
messages(listener: (message: HouseAppMessage) => void): () => void;
|
|
43
|
-
gaConsent(choice: HouseGaChoice): void;
|
|
44
|
-
}
|
|
45
|
-
export declare function connectHouseBridge(source?: HouseBridgeSource): HouseBridge;
|
|
46
|
-
export {};
|
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
export class HouseCeremonyRefused extends Error {
|
|
2
|
-
path;
|
|
3
|
-
constructor(path) {
|
|
4
|
-
super('house_ceremony_refused');
|
|
5
|
-
this.name = 'HouseCeremonyRefused';
|
|
6
|
-
this.path = path;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
export class HouseNavigationRefused extends Error {
|
|
10
|
-
address;
|
|
11
|
-
constructor(address) {
|
|
12
|
-
super('house_navigation_refused');
|
|
13
|
-
this.name = 'HouseNavigationRefused';
|
|
14
|
-
this.address = address;
|
|
15
|
-
}
|
|
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
|
-
}
|
|
31
|
-
function settled() {
|
|
32
|
-
let settle = null;
|
|
33
|
-
const promise = new Promise((resolve, reject) => {
|
|
34
|
-
settle = { resolve, reject };
|
|
35
|
-
});
|
|
36
|
-
return { promise, settle: settle };
|
|
37
|
-
}
|
|
38
|
-
function heldBridge() {
|
|
39
|
-
const held = globalThis;
|
|
40
|
-
if (held.houseBridge === undefined || held.houseRuntime === undefined)
|
|
41
|
-
throw new Error('house_bridge_absent');
|
|
42
|
-
return { port: held.houseBridge, runtime: held.houseRuntime };
|
|
43
|
-
}
|
|
44
|
-
export function connectHouseBridge(source = heldBridge()) {
|
|
45
|
-
const ceremonies = [];
|
|
46
|
-
const navigations = [];
|
|
47
|
-
const attachments = new Map();
|
|
48
|
-
let nextAttachment = 0;
|
|
49
|
-
const sources = new Map();
|
|
50
|
-
let nextSource = 0;
|
|
51
|
-
const installations = new Map();
|
|
52
|
-
let nextInstallation = 0;
|
|
53
|
-
const notifications = new Map();
|
|
54
|
-
let nextNotifications = 0;
|
|
55
|
-
const badges = new Map();
|
|
56
|
-
let nextBadge = 0;
|
|
57
|
-
const screenshots = new Map();
|
|
58
|
-
let nextScreenshot = 0;
|
|
59
|
-
const externals = new Map();
|
|
60
|
-
let nextExternal = 0;
|
|
61
|
-
const addressListeners = new Set();
|
|
62
|
-
const statusListeners = new Set();
|
|
63
|
-
const messageListeners = new Set();
|
|
64
|
-
let outbox = [];
|
|
65
|
-
let held = null;
|
|
66
|
-
let address = null;
|
|
67
|
-
let status = null;
|
|
68
|
-
function send(message) {
|
|
69
|
-
if (held === null)
|
|
70
|
-
outbox?.push(message);
|
|
71
|
-
else
|
|
72
|
-
held.postMessage(message);
|
|
73
|
-
}
|
|
74
|
-
function receive(event) {
|
|
75
|
-
const data = event.data;
|
|
76
|
-
if (data === null || typeof data !== 'object')
|
|
77
|
-
return;
|
|
78
|
-
if (data.house === 'status') {
|
|
79
|
-
status = data.status;
|
|
80
|
-
for (const listener of statusListeners)
|
|
81
|
-
listener(status);
|
|
82
|
-
return;
|
|
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
|
-
}
|
|
90
|
-
if (data.house === 'address') {
|
|
91
|
-
address = String(data.address);
|
|
92
|
-
if (data.navigated === true)
|
|
93
|
-
navigations.shift()?.settle.resolve(address);
|
|
94
|
-
for (const listener of addressListeners)
|
|
95
|
-
listener(address);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (data.house === 'navigate-refused') {
|
|
99
|
-
const refused = data.address;
|
|
100
|
-
const named = typeof refused === 'string' ? refused : null;
|
|
101
|
-
const index = named === null ? 0 : navigations.findIndex((one) => one.address === named);
|
|
102
|
-
const [pending] = navigations.splice(index < 0 ? 0 : index, 1);
|
|
103
|
-
pending?.settle.reject(new HouseNavigationRefused(named));
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
if (data.house === 'attachment') {
|
|
107
|
-
const answer = data;
|
|
108
|
-
if (typeof answer.id !== 'number')
|
|
109
|
-
return;
|
|
110
|
-
const pending = attachments.get(answer.id);
|
|
111
|
-
if (pending === undefined)
|
|
112
|
-
return;
|
|
113
|
-
attachments.delete(answer.id);
|
|
114
|
-
pending.resolve(answer.image ?? null);
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
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') {
|
|
132
|
-
const answer = data;
|
|
133
|
-
if (typeof answer.id !== 'number')
|
|
134
|
-
return;
|
|
135
|
-
const pending = sources.get(answer.id);
|
|
136
|
-
if (pending === undefined)
|
|
137
|
-
return;
|
|
138
|
-
sources.delete(answer.id);
|
|
139
|
-
pending.resolve(answer.answer ?? null);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (data.house === 'external-call') {
|
|
143
|
-
const answer = data;
|
|
144
|
-
if (typeof answer.id !== 'number')
|
|
145
|
-
return;
|
|
146
|
-
const pending = externals.get(answer.id);
|
|
147
|
-
if (pending === undefined)
|
|
148
|
-
return;
|
|
149
|
-
externals.delete(answer.id);
|
|
150
|
-
pending.resolve(answer.answer ?? null);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
if (data.house === 'screenshot') {
|
|
154
|
-
const answer = data;
|
|
155
|
-
if (typeof answer.id !== 'number')
|
|
156
|
-
return;
|
|
157
|
-
const pending = screenshots.get(answer.id);
|
|
158
|
-
if (pending === undefined)
|
|
159
|
-
return;
|
|
160
|
-
screenshots.delete(answer.id);
|
|
161
|
-
pending.resolve(answer.image ?? null);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (data.house === 'ceremony-refused') {
|
|
165
|
-
const refused = data.path;
|
|
166
|
-
const named = typeof refused === 'string' ? refused : null;
|
|
167
|
-
const index = named === null ? 0 : ceremonies.findIndex((one) => one.path === named);
|
|
168
|
-
const [pending] = ceremonies.splice(index < 0 ? 0 : index, 1);
|
|
169
|
-
pending?.settle.reject(new HouseCeremonyRefused(named));
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
void source.port.then((port) => {
|
|
173
|
-
held = port;
|
|
174
|
-
port.addEventListener('message', receive);
|
|
175
|
-
port.start();
|
|
176
|
-
const waiting = outbox ?? [];
|
|
177
|
-
outbox = null;
|
|
178
|
-
for (const message of waiting)
|
|
179
|
-
port.postMessage(message);
|
|
180
|
-
});
|
|
181
|
-
function readSource(house, app, release) {
|
|
182
|
-
const pending = settled();
|
|
183
|
-
const id = ++nextSource;
|
|
184
|
-
sources.set(id, pending.settle);
|
|
185
|
-
send({ house, id, app, release });
|
|
186
|
-
return pending.promise;
|
|
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
|
-
}
|
|
195
|
-
function navigate(mode, asked) {
|
|
196
|
-
const pending = settled();
|
|
197
|
-
navigations.push({ address: asked, settle: pending.settle });
|
|
198
|
-
send({ house: 'navigate', mode, address: asked });
|
|
199
|
-
return pending.promise;
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
runtime: source.runtime,
|
|
203
|
-
address() {
|
|
204
|
-
return address;
|
|
205
|
-
},
|
|
206
|
-
onAddress(listener) {
|
|
207
|
-
addressListeners.add(listener);
|
|
208
|
-
return () => {
|
|
209
|
-
addressListeners.delete(listener);
|
|
210
|
-
};
|
|
211
|
-
},
|
|
212
|
-
push(asked) {
|
|
213
|
-
return navigate('push', asked);
|
|
214
|
-
},
|
|
215
|
-
replace(asked) {
|
|
216
|
-
return navigate('replace', asked);
|
|
217
|
-
},
|
|
218
|
-
ceremony(path) {
|
|
219
|
-
const pending = settled();
|
|
220
|
-
ceremonies.push({ path, settle: pending.settle });
|
|
221
|
-
send({ house: 'ceremony', path });
|
|
222
|
-
return pending.promise;
|
|
223
|
-
},
|
|
224
|
-
install() {
|
|
225
|
-
const pending = settled();
|
|
226
|
-
const id = ++nextInstallation;
|
|
227
|
-
installations.set(id, pending.settle);
|
|
228
|
-
send({ house: 'install', id });
|
|
229
|
-
return pending.promise;
|
|
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
|
-
},
|
|
245
|
-
setTab(tab) {
|
|
246
|
-
send({ house: 'tab', title: tab.title, icon: tab.icon });
|
|
247
|
-
},
|
|
248
|
-
readAttachment(room, attachment) {
|
|
249
|
-
const pending = settled();
|
|
250
|
-
const id = ++nextAttachment;
|
|
251
|
-
attachments.set(id, pending.settle);
|
|
252
|
-
send({ house: 'attachment-read', id, room, attachment });
|
|
253
|
-
return pending.promise;
|
|
254
|
-
},
|
|
255
|
-
sourceTop(app, release) {
|
|
256
|
-
return readSource('source-top', app, release);
|
|
257
|
-
},
|
|
258
|
-
sourceDownload(app, release) {
|
|
259
|
-
return readSource('source-download', app, release);
|
|
260
|
-
},
|
|
261
|
-
callExternal(request) {
|
|
262
|
-
const pending = settled();
|
|
263
|
-
const id = ++nextExternal;
|
|
264
|
-
externals.set(id, pending.settle);
|
|
265
|
-
send({ house: 'external-call', id, request });
|
|
266
|
-
return pending.promise;
|
|
267
|
-
},
|
|
268
|
-
readScreenshot(app) {
|
|
269
|
-
return screenshot('screenshot-read', { app });
|
|
270
|
-
},
|
|
271
|
-
readSiteScreenshot(site) {
|
|
272
|
-
return screenshot('site-screenshot-read', { site });
|
|
273
|
-
},
|
|
274
|
-
status() {
|
|
275
|
-
return status;
|
|
276
|
-
},
|
|
277
|
-
onStatus(listener) {
|
|
278
|
-
statusListeners.add(listener);
|
|
279
|
-
return () => {
|
|
280
|
-
statusListeners.delete(listener);
|
|
281
|
-
};
|
|
282
|
-
},
|
|
283
|
-
messages(listener) {
|
|
284
|
-
messageListeners.add(listener);
|
|
285
|
-
return () => {
|
|
286
|
-
messageListeners.delete(listener);
|
|
287
|
-
};
|
|
288
|
-
},
|
|
289
|
-
gaConsent(choice) {
|
|
290
|
-
send({ house: 'ga/consent', choice });
|
|
291
|
-
},
|
|
292
|
-
};
|
|
293
|
-
}
|
|
@@ -1,3 +0,0 @@
|
|
|
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';
|
package/dist/client/src/index.js
DELETED
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { appAddressOf } from '../../src/apps/remainder.js';
|
|
2
|
-
import { tabChange } from '../../src/apps/tab.js';
|
|
3
|
-
import { HOUSE_ADDRESS_MAX_CHARS } from './wire.js';
|
|
4
|
-
const HOST_ORIGIN = 'https://house.invalid';
|
|
5
|
-
function sameOriginPath(value) {
|
|
6
|
-
if (typeof value !== 'string')
|
|
7
|
-
return null;
|
|
8
|
-
if (value.charAt(0) !== '/' || value.slice(0, 2) === '//')
|
|
9
|
-
return null;
|
|
10
|
-
if (value.length > HOUSE_ADDRESS_MAX_CHARS)
|
|
11
|
-
return null;
|
|
12
|
-
const target = new URL(value, HOST_ORIGIN);
|
|
13
|
-
return target.origin === HOST_ORIGIN ? `${target.pathname}${target.search}${target.hash}` : null;
|
|
14
|
-
}
|
|
15
|
-
export function createHouseBridgeTestHost(options = {}) {
|
|
16
|
-
const navigations = [];
|
|
17
|
-
const ceremonies = [];
|
|
18
|
-
const consents = [];
|
|
19
|
-
const tabs = [];
|
|
20
|
-
const externalCalls = [];
|
|
21
|
-
const listeners = new Set();
|
|
22
|
-
const queued = [];
|
|
23
|
-
let started = false;
|
|
24
|
-
let address = appAddressOf('/', options.address ?? '/');
|
|
25
|
-
let status = options.status ?? null;
|
|
26
|
-
let attached = () => { };
|
|
27
|
-
const runtime = new Promise((resolve) => {
|
|
28
|
-
attached = resolve;
|
|
29
|
-
});
|
|
30
|
-
function deliver(data) {
|
|
31
|
-
const event = { data };
|
|
32
|
-
if (!started) {
|
|
33
|
-
queued.push(event);
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
queueMicrotask(() => {
|
|
37
|
-
for (const listener of listeners)
|
|
38
|
-
listener(event);
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
function arrive(target, navigated) {
|
|
42
|
-
address = target;
|
|
43
|
-
deliver({ house: 'address', address, navigated });
|
|
44
|
-
}
|
|
45
|
-
function navigate(mode, asked) {
|
|
46
|
-
const target = sameOriginPath(asked);
|
|
47
|
-
if (target === null) {
|
|
48
|
-
deliver({
|
|
49
|
-
house: 'navigate-refused',
|
|
50
|
-
address: typeof asked === 'string' ? asked : null,
|
|
51
|
-
});
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
navigations.push({ mode: mode === 'replace' ? 'replace' : 'push', address: target });
|
|
55
|
-
arrive(target, true);
|
|
56
|
-
}
|
|
57
|
-
function ceremony(path) {
|
|
58
|
-
const target = sameOriginPath(path);
|
|
59
|
-
if (target === null) {
|
|
60
|
-
deliver({ house: 'ceremony-refused', path: typeof path === 'string' ? path : null });
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
ceremonies.push(target);
|
|
64
|
-
}
|
|
65
|
-
function publish(next) {
|
|
66
|
-
status = next;
|
|
67
|
-
deliver({ house: 'status', status });
|
|
68
|
-
}
|
|
69
|
-
function consent(choice) {
|
|
70
|
-
if (status === null || !status.ga.measures)
|
|
71
|
-
return;
|
|
72
|
-
if (choice !== 'accept' && choice !== 'decline')
|
|
73
|
-
return;
|
|
74
|
-
consents.push(choice);
|
|
75
|
-
publish({ ...status, ga: { measures: true, choice } });
|
|
76
|
-
}
|
|
77
|
-
function receive(message) {
|
|
78
|
-
const data = message;
|
|
79
|
-
if (data === null || typeof data !== 'object')
|
|
80
|
-
return;
|
|
81
|
-
if (data.house === 'navigate') {
|
|
82
|
-
navigate(data.mode, data.address);
|
|
83
|
-
}
|
|
84
|
-
else if (data.house === 'ceremony') {
|
|
85
|
-
ceremony(data.path);
|
|
86
|
-
}
|
|
87
|
-
else if (data.house === 'install') {
|
|
88
|
-
deliver({ house: 'install', id: data.id, outcome: 'manual' });
|
|
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
|
-
}
|
|
96
|
-
else if (data.house === 'tab') {
|
|
97
|
-
const change = tabChange(data);
|
|
98
|
-
if (change !== null)
|
|
99
|
-
tabs.push(change);
|
|
100
|
-
}
|
|
101
|
-
else if (data.house === 'ga/consent') {
|
|
102
|
-
consent(data.choice);
|
|
103
|
-
}
|
|
104
|
-
else if (data.house === 'source-top' || data.house === 'source-download') {
|
|
105
|
-
const asked = data;
|
|
106
|
-
const source = options.sources?.find((entry) => entry.top.app_ref === asked.app);
|
|
107
|
-
const answer = source === undefined ? null : data.house === 'source-top' ? source.top : source.download;
|
|
108
|
-
deliver({ house: 'source', id: asked.id, answer });
|
|
109
|
-
}
|
|
110
|
-
else if (data.house === 'attachment-read') {
|
|
111
|
-
const asked = data;
|
|
112
|
-
const image = options.images?.find((entry) => entry.room === asked.room && entry.attachment === asked.attachment);
|
|
113
|
-
deliver({
|
|
114
|
-
house: 'attachment',
|
|
115
|
-
id: asked.id,
|
|
116
|
-
image: image === undefined
|
|
117
|
-
? null
|
|
118
|
-
: {
|
|
119
|
-
attachment: image.attachment,
|
|
120
|
-
copy: image.copy,
|
|
121
|
-
name: image.name,
|
|
122
|
-
media_type: image.media_type,
|
|
123
|
-
},
|
|
124
|
-
});
|
|
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
|
-
}
|
|
131
|
-
else if (data.house === 'screenshot-read') {
|
|
132
|
-
const asked = data;
|
|
133
|
-
const held = typeof asked.app === 'string' ? options.screenshots?.[asked.app] : undefined;
|
|
134
|
-
deliver({ house: 'screenshot', id: asked.id, image: held ?? null });
|
|
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
|
-
}
|
|
141
|
-
}
|
|
142
|
-
const port = {
|
|
143
|
-
postMessage(message) {
|
|
144
|
-
receive(message);
|
|
145
|
-
},
|
|
146
|
-
addEventListener(_type, listener) {
|
|
147
|
-
listeners.add(listener);
|
|
148
|
-
},
|
|
149
|
-
start() {
|
|
150
|
-
started = true;
|
|
151
|
-
const held = queued.splice(0);
|
|
152
|
-
queueMicrotask(() => {
|
|
153
|
-
for (const event of held) {
|
|
154
|
-
for (const listener of listeners)
|
|
155
|
-
listener(event);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
if (status !== null)
|
|
161
|
-
deliver({ house: 'status', status });
|
|
162
|
-
deliver({ house: 'address', address, navigated: false });
|
|
163
|
-
return {
|
|
164
|
-
port: Promise.resolve(port),
|
|
165
|
-
runtime,
|
|
166
|
-
navigations,
|
|
167
|
-
ceremonies,
|
|
168
|
-
consents,
|
|
169
|
-
tabs,
|
|
170
|
-
externalCalls,
|
|
171
|
-
address() {
|
|
172
|
-
return address;
|
|
173
|
-
},
|
|
174
|
-
open(asked) {
|
|
175
|
-
arrive(appAddressOf('/', asked), false);
|
|
176
|
-
},
|
|
177
|
-
publish,
|
|
178
|
-
message(message) {
|
|
179
|
-
deliver({ house: 'app-message', from: message.from, body: message.body });
|
|
180
|
-
},
|
|
181
|
-
attachRuntime(client) {
|
|
182
|
-
attached(client);
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import type { TabChange } from '../../src/apps/tab.js';
|
|
2
|
-
export interface HouseBridgeEvent {
|
|
3
|
-
readonly data: unknown;
|
|
4
|
-
}
|
|
5
|
-
export interface HouseBridgePort {
|
|
6
|
-
postMessage(message: unknown): void;
|
|
7
|
-
addEventListener(type: 'message', listener: (event: HouseBridgeEvent) => void): void;
|
|
8
|
-
start(): void;
|
|
9
|
-
}
|
|
10
|
-
export interface HouseSyncState {
|
|
11
|
-
readonly state: string;
|
|
12
|
-
readonly providerUnreachable: boolean;
|
|
13
|
-
}
|
|
14
|
-
export interface HouseCredentialState {
|
|
15
|
-
readonly state: 'none' | 'held' | 'rejected';
|
|
16
|
-
readonly expires_at: number | null;
|
|
17
|
-
}
|
|
18
|
-
export type HouseGaChoice = 'accept' | 'decline';
|
|
19
|
-
export interface HouseGa {
|
|
20
|
-
readonly measures: boolean;
|
|
21
|
-
readonly choice: HouseGaChoice | null;
|
|
22
|
-
}
|
|
23
|
-
export interface HouseStatus {
|
|
24
|
-
readonly app: string;
|
|
25
|
-
readonly runtime: string;
|
|
26
|
-
readonly credential: HouseCredentialState;
|
|
27
|
-
readonly sync: HouseSyncState | null;
|
|
28
|
-
readonly refusal: {
|
|
29
|
-
readonly reason: string;
|
|
30
|
-
} | null;
|
|
31
|
-
readonly loss: string | null;
|
|
32
|
-
readonly ga: HouseGa;
|
|
33
|
-
readonly service_mode: 'open' | 'write-halted' | null;
|
|
34
|
-
}
|
|
35
|
-
export type HouseNavigationMode = 'push' | 'replace';
|
|
36
|
-
interface HouseRelease {
|
|
37
|
-
readonly app_ref: string;
|
|
38
|
-
readonly release: string;
|
|
39
|
-
}
|
|
40
|
-
export interface HouseSourceTop extends HouseRelease {
|
|
41
|
-
readonly folders: readonly string[];
|
|
42
|
-
readonly files: readonly {
|
|
43
|
-
readonly name: string;
|
|
44
|
-
readonly size: number;
|
|
45
|
-
}[];
|
|
46
|
-
readonly readme: string | null;
|
|
47
|
-
}
|
|
48
|
-
export type HouseSourceFile = {
|
|
49
|
-
readonly path: string;
|
|
50
|
-
readonly size: number;
|
|
51
|
-
} & ({
|
|
52
|
-
readonly text: string;
|
|
53
|
-
} | {
|
|
54
|
-
readonly base64: string;
|
|
55
|
-
});
|
|
56
|
-
export interface HouseSourceDownload extends HouseRelease {
|
|
57
|
-
readonly files: readonly HouseSourceFile[];
|
|
58
|
-
}
|
|
59
|
-
export type HouseTab = TabChange;
|
|
60
|
-
export interface HouseExternalRequest {
|
|
61
|
-
readonly url: string;
|
|
62
|
-
readonly method?: string;
|
|
63
|
-
readonly headers?: Readonly<Record<string, string>>;
|
|
64
|
-
readonly body?: string;
|
|
65
|
-
readonly body_base64?: string;
|
|
66
|
-
readonly account?: string;
|
|
67
|
-
readonly slot?: string;
|
|
68
|
-
}
|
|
69
|
-
export type HouseExternalAnswer = {
|
|
70
|
-
readonly status: number;
|
|
71
|
-
readonly headers: Readonly<Record<string, string | readonly string[]>>;
|
|
72
|
-
} & ({
|
|
73
|
-
readonly body: string;
|
|
74
|
-
} | {
|
|
75
|
-
readonly body_base64: string;
|
|
76
|
-
});
|
|
77
|
-
export interface HouseRefusal {
|
|
78
|
-
readonly error: {
|
|
79
|
-
readonly code: string;
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
export interface HouseAppMessage {
|
|
83
|
-
readonly from: string;
|
|
84
|
-
readonly body: unknown;
|
|
85
|
-
}
|
|
86
|
-
export declare const HOUSE_ADDRESS_MAX_CHARS = 1000;
|
|
87
|
-
export {};
|
package/dist/client/src/wire.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const HOUSE_ADDRESS_MAX_CHARS = 1000;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
function split(address) {
|
|
2
|
-
const mark = address.search(/[?#]/u);
|
|
3
|
-
return mark < 0
|
|
4
|
-
? { path: address, rest: '' }
|
|
5
|
-
: { path: address.slice(0, mark), rest: address.slice(mark) };
|
|
6
|
-
}
|
|
7
|
-
export function appAddressOf(root, address) {
|
|
8
|
-
const { path, rest } = split(address);
|
|
9
|
-
const below = root === '/' ? path : path.slice(root.length);
|
|
10
|
-
return `${below.startsWith('/') ? below : `/${below}`}${rest}`;
|
|
11
|
-
}
|
|
12
|
-
export function shellAddressOf(root, appAddress) {
|
|
13
|
-
const { path, rest } = split(appAddress);
|
|
14
|
-
const below = path === '/' ? '' : path;
|
|
15
|
-
return `${`${root === '/' ? '' : root}${below}` || '/'}${rest}`;
|
|
16
|
-
}
|
package/dist/src/apps/tab.d.ts
DELETED
package/dist/src/apps/tab.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|